Back out 27fb990d7fc7 (bug 838374) for Android bustage
authorPhil Ringnalda <philringnalda@gmail.com>
Thu, 21 Mar 2013 22:58:41 -0700
changeset 136308 10080f945066f0dd39c8a9af51165d951336169e
parent 136307 0a2d1bfae2b406024aeab53527b23882a9ed7910
child 136309 eb1e425dbf0fbe484cd10b6b2649b34ca2ebcdf2
push idunknown
push userunknown
push dateunknown
bugs838374
milestone22.0a1
backs out27fb990d7fc75fc3a21755cf520583381d11e9d2
Back out 27fb990d7fc7 (bug 838374) for Android bustage CLOSED TREE
testing/mozbase/mozcrash/mozcrash/mozcrash.py
testing/mozbase/mozcrash/setup.py
testing/mozbase/mozdevice/README.md
testing/mozbase/mozdevice/mozdevice/__init__.py
testing/mozbase/mozdevice/mozdevice/b2gemulator.py
testing/mozbase/mozdevice/mozdevice/devicemanager.py
testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
testing/mozbase/mozdevice/mozdevice/dmcli.py
testing/mozbase/mozdevice/mozdevice/droid.py
testing/mozbase/mozdevice/mozdevice/emulator.py
testing/mozbase/mozdevice/mozdevice/emulator_battery.py
testing/mozbase/mozdevice/mozdevice/sutini.py
testing/mozbase/mozdevice/setup.py
testing/mozbase/mozdevice/sut_tests/dmunit.py
testing/mozbase/mozdevice/sut_tests/runtests.py
testing/mozbase/mozdevice/sut_tests/test_cat2.py
testing/mozbase/mozdevice/sut_tests/test_datachannel.py
testing/mozbase/mozdevice/sut_tests/test_exec.py
testing/mozbase/mozdevice/sut_tests/test_exec_env.py
testing/mozbase/mozdevice/sut_tests/test_getdir.py
testing/mozbase/mozdevice/sut_tests/test_info.py
testing/mozbase/mozdevice/sut_tests/test_isdir.py
testing/mozbase/mozdevice/sut_tests/test_prompt.py
testing/mozbase/mozdevice/sut_tests/test_ps.py
testing/mozbase/mozdevice/sut_tests/test_pull.py
testing/mozbase/mozdevice/sut_tests/test_push1.py
testing/mozbase/mozdevice/sut_tests/test_push2.py
testing/mozbase/mozdevice/sut_tests/test_pushbinary.py
testing/mozbase/mozdevice/sut_tests/test_pushsmalltext.py
testing/mozbase/mozdevice/tests/sut_mkdir.py
testing/mozbase/mozfile/README.md
testing/mozbase/mozfile/mozfile/mozfile.py
testing/mozbase/mozfile/setup.py
testing/mozbase/mozfile/tests/is_url.py
testing/mozbase/mozfile/tests/manifest.ini
testing/mozbase/mozprocess/README.md
testing/mozbase/mozprocess/setup.py
testing/mozbase/mozprocess/tests/manifest.ini
testing/mozbase/mozprocess/tests/mozprocess1.py
testing/mozbase/mozprocess/tests/mozprocess2.py
testing/mozbase/mozprocess/tests/test_mozprocess.py
testing/mozbase/mozprofile/README.md
testing/mozbase/mozprofile/mozprofile/__init__.py
testing/mozbase/mozprofile/mozprofile/addons.py
testing/mozbase/mozprofile/mozprofile/cli.py
testing/mozbase/mozprofile/mozprofile/permissions.py
testing/mozbase/mozprofile/mozprofile/prefs.py
testing/mozbase/mozprofile/mozprofile/profile.py
testing/mozbase/mozprofile/mozprofile/webapps.py
testing/mozbase/mozprofile/setup.py
testing/mozbase/mozprofile/tests/bug785146.py
testing/mozbase/mozprofile/tests/files/prefs_with_comments.js
testing/mozbase/mozprofile/tests/files/webapps1.json
testing/mozbase/mozprofile/tests/files/webapps2.json
testing/mozbase/mozprofile/tests/manifest.ini
testing/mozbase/mozprofile/tests/test_clone_cleanup.py
testing/mozbase/mozprofile/tests/test_preferences.py
testing/mozbase/mozprofile/tests/test_webapps.py
testing/mozbase/mozrunner/README.md
testing/mozbase/mozrunner/setup.py
--- a/testing/mozbase/mozcrash/mozcrash/mozcrash.py
+++ b/testing/mozbase/mozcrash/mozcrash/mozcrash.py
@@ -1,27 +1,52 @@
 # 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/.
 
 __all__ = ['check_for_crashes']
 
-import glob
+import os, sys, glob, urllib2, tempfile, re, subprocess, shutil, urlparse, zipfile
 import mozlog
-import os
-import re
-import shutil
-import subprocess
-import sys
-import tempfile
-import urllib2
-import zipfile
+
+def is_url(thing):
+  """
+  Return True if thing looks like a URL.
+  """
+  # We want to download URLs like http://... but not Windows paths like c:\...
+  parsed = urlparse.urlparse(thing)
+  if 'scheme' in parsed:
+      return len(parsed.scheme) >= 2
+  else:
+      return len(parsed[0]) >= 2
 
-from mozfile import extract_zip
-from mozfile import is_url
+def extractall(zip, path = None):
+    """
+    Compatibility shim for Python 2.6's ZipFile.extractall.
+    """
+    if hasattr(zip, "extractall"):
+        return zip.extractall(path)
+
+    if path is None:
+        path = os.curdir
+
+    for name in self._zipfile.namelist():
+        filename = os.path.normpath(os.path.join(path, name))
+        if name.endswith("/"):
+            os.makedirs(filename)
+        else:
+            path = os.path.split(filename)[0]
+            if not os.path.isdir(path):
+                os.makedirs(path)
+
+        try:
+            f = open(filename, "wb")
+            f.write(zip.read(name))
+        finally:
+            f.close()
 
 def check_for_crashes(dump_directory, symbols_path,
                       stackwalk_binary=None,
                       dump_save_path=None,
                       test_name=None):
     """
     Print a stack trace for minidump files left behind by a crashing program.
 
@@ -31,59 +56,59 @@ def check_for_crashes(dump_directory, sy
 
     `stackwalk_binary` should be a path to the minidump_stackwalk binary.
     If `stackwalk_binary` is not set, the MINIDUMP_STACKWALK environment variable
     will be checked and its value used if it is not empty.
 
     `symbols_path` should be a path to a directory containing symbols to use for
     dump processing. This can either be a path to a directory containing Breakpad-format
     symbols, or a URL to a zip file containing a set of symbols.
-
+                  
     If `dump_save_path` is set, it should be a path to a directory in which to copy minidump
     files for safekeeping after a stack trace has been printed. If not set, the environment
     variable MINIDUMP_SAVE_PATH will be checked and its value used if it is not empty.
-
+                    
     If `test_name` is set it will be used as the test name in log output. If not set the
     filename of the calling function will be used.
 
     Returns True if any minidumps were found, False otherwise.
     """
-    dumps = glob.glob(os.path.join(dump_directory, '*.dmp'))
-    if not dumps:
-        return False
-
+    log = mozlog.getLogger('mozcrash')
     if stackwalk_binary is None:
         stackwalk_binary = os.environ.get('MINIDUMP_STACKWALK', None)
 
     # try to get the caller's filename if no test name is given
     if test_name is None:
         try:
             test_name = os.path.basename(sys._getframe(1).f_code.co_filename)
         except:
             test_name = "unknown"
 
+    # Check preconditions
+    dumps = glob.glob(os.path.join(dump_directory, '*.dmp'))
+    if len(dumps) == 0:
+        return False
+
+    remove_symbols = False 
+    # If our symbols are at a remote URL, download them now
+    if symbols_path and is_url(symbols_path):
+        log.info("Downloading symbols from: %s", symbols_path)
+        remove_symbols = True
+        # Get the symbols and write them to a temporary zipfile
+        data = urllib2.urlopen(symbols_path)
+        symbols_file = tempfile.TemporaryFile()
+        symbols_file.write(data.read())
+        # extract symbols to a temporary directory (which we'll delete after
+        # processing all crashes)
+        symbols_path = tempfile.mkdtemp()
+        zfile = zipfile.ZipFile(symbols_file, 'r')
+        extractall(zfile, symbols_path)
+        zfile.close()
+
     try:
-        log = mozlog.getLogger('mozcrash')
-        remove_symbols = False
-        # If our symbols are at a remote URL, download them now
-        # We want to download URLs like http://... but not Windows paths like c:\...
-        if symbols_path and is_url(symbols_path):
-            log.info("Downloading symbols from: %s", symbols_path)
-            remove_symbols = True
-            # Get the symbols and write them to a temporary zipfile
-            data = urllib2.urlopen(symbols_path)
-            symbols_file = tempfile.TemporaryFile()
-            symbols_file.write(data.read())
-            # extract symbols to a temporary directory (which we'll delete after
-            # processing all crashes)
-            symbols_path = tempfile.mkdtemp()
-            zfile = zipfile.ZipFile(symbols_file, 'r')
-            extract_zip(zfile, symbols_path)
-            zfile.close()
-
         for d in dumps:
             stackwalk_output = []
             stackwalk_output.append("Crash dump filename: " + d)
             top_frame = None
             if symbols_path and stackwalk_binary and os.path.exists(stackwalk_binary):
                 # run minidump_stackwalk
                 p = subprocess.Popen([stackwalk_binary, d, symbols_path],
                                      stdout=subprocess.PIPE,
@@ -115,17 +140,17 @@ def check_for_crashes(dump_directory, sy
                 if not symbols_path:
                     stackwalk_output.append("No symbols path given, can't process dump.")
                 if not stackwalk_binary:
                     stackwalk_output.append("MINIDUMP_STACKWALK not set, can't process dump.")
                 elif stackwalk_binary and not os.path.exists(stackwalk_binary):
                     stackwalk_output.append("MINIDUMP_STACKWALK binary not found: %s" % stackwalk_binary)
             if not top_frame:
                 top_frame = "Unknown top frame"
-            print "PROCESS-CRASH | %s | application crashed [%s]" % (test_name, top_frame)
+            log.error("PROCESS-CRASH | %s | application crashed [%s]", test_name, top_frame)
             print '\n'.join(stackwalk_output)
             if dump_save_path is None:
                 dump_save_path = os.environ.get('MINIDUMP_SAVE_PATH', None)
             if dump_save_path:
                 shutil.move(d, dump_save_path)
                 log.info("Saved dump as %s", os.path.join(dump_save_path,
                                                           os.path.basename(d)))
             else:
--- a/testing/mozbase/mozcrash/setup.py
+++ b/testing/mozbase/mozcrash/setup.py
@@ -1,19 +1,18 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
-PACKAGE_VERSION = '0.5'
+PACKAGE_VERSION = '0.3'
 
 # dependencies
-deps = ['mozfile >= 0.3',
-        'mozlog']
+deps = []
 
 setup(name='mozcrash',
       version=PACKAGE_VERSION,
       description="Library for printing stack traces from minidumps left behind by crashed processes",
       long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/README.md
@@ -0,0 +1,5 @@
+[mozdevice](https://github.com/mozilla/mozbase/tree/master/mozdevice) provides
+an interface to interact with a remote device such as an Android phone connected
+to a workstation. Currently there are two implementations of the interface: one
+uses a TCP-based protocol to communicate with a server running on the device,
+another uses Android's adb utility.
--- a/testing/mozbase/mozdevice/mozdevice/__init__.py
+++ b/testing/mozbase/mozdevice/mozdevice/__init__.py
@@ -1,8 +1,10 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
-from devicemanager import DeviceManager, DMError
+from devicemanager import DMError
 from devicemanagerADB import DeviceManagerADB
 from devicemanagerSUT import DeviceManagerSUT
 from droid import DroidADB, DroidSUT, DroidConnectByHWID
+from emulator import Emulator
+from b2gemulator import B2GEmulator
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/b2gemulator.py
@@ -0,0 +1,87 @@
+# 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 os
+import platform
+
+from emulator import Emulator
+
+
+class B2GEmulator(Emulator):
+
+    def __init__(self, homedir=None, noWindow=False, logcat_dir=None, arch="x86",
+                 emulatorBinary=None, res='480x800', userdata=None,
+                 memory='512', partition_size='512'):
+        super(B2GEmulator, self).__init__(noWindow=noWindow, logcat_dir=logcat_dir,
+                                          arch=arch, emulatorBinary=emulatorBinary,
+                                          res=res, userdata=userdata,
+                                          memory=memory, partition_size=partition_size)
+        self.homedir = homedir
+        if self.homedir is not None:
+            self.homedir = os.path.expanduser(homedir)
+
+    def _check_file(self, filePath):
+        if not os.path.exists(filePath):
+            raise Exception(('File not found: %s; did you pass the B2G home '
+                             'directory as the homedir parameter, or set '
+                             'B2G_HOME correctly?') % filePath)
+
+    def _check_for_adb(self, host_dir):
+        if self._default_adb() == 0:
+            return
+        adb_paths = [os.path.join(self.homedir,'glue','gonk','out','host',
+                      host_dir ,'bin','adb'),os.path.join(self.homedir, 'out',
+                      'host', host_dir,'bin','adb'),os.path.join(self.homedir,
+                      'bin','adb')]
+        for option in adb_paths:
+            if os.path.exists(option):
+                self.adb = option
+                return
+        raise Exception('adb not found!')
+
+    def _locate_files(self):
+        if self.homedir is None:
+            self.homedir = os.getenv('B2G_HOME')
+        if self.homedir is None:
+            raise Exception('Must define B2G_HOME or pass the homedir parameter')
+        self._check_file(self.homedir)
+
+        if self.arch not in ("x86", "arm"):
+            raise Exception("Emulator architecture must be one of x86, arm, got: %s" %
+                            self.arch)
+
+        host_dir = "linux-x86"
+        if platform.system() == "Darwin":
+            host_dir = "darwin-x86"
+
+        host_bin_dir = os.path.join("out", "host", host_dir, "bin")
+
+        if self.arch == "x86":
+            binary = os.path.join(host_bin_dir, "emulator-x86")
+            kernel = "prebuilts/qemu-kernel/x86/kernel-qemu"
+            sysdir = "out/target/product/generic_x86"
+            self.tail_args = []
+        else:
+            binary = os.path.join(host_bin_dir, "emulator")
+            kernel = "prebuilts/qemu-kernel/arm/kernel-qemu-armv7"
+            sysdir = "out/target/product/generic"
+            self.tail_args = ["-cpu", "cortex-a8"]
+
+        self._check_for_adb(host_dir)
+
+        if not self.binary:
+            self.binary = os.path.join(self.homedir, binary)
+
+        self._check_file(self.binary)
+
+        self.kernelImg = os.path.join(self.homedir, kernel)
+        self._check_file(self.kernelImg)
+
+        self.sysDir = os.path.join(self.homedir, sysdir)
+        self._check_file(self.sysDir)
+
+        if not self.dataImg:
+            self.dataImg = os.path.join(self.sysDir, 'userdata.img')
+        self._check_file(self.dataImg)
--- a/testing/mozbase/mozdevice/mozdevice/devicemanager.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
@@ -6,115 +6,534 @@ import hashlib
 import socket
 import os
 import re
 import struct
 import StringIO
 import zlib
 
 from Zeroconf import Zeroconf, ServiceBrowser
-from functools import wraps
 
 class DMError(Exception):
     "generic devicemanager exception."
 
     def __init__(self, msg= '', fatal = False):
         self.msg = msg
         self.fatal = fatal
 
     def __str__(self):
         return self.msg
 
 def abstractmethod(method):
     line = method.func_code.co_firstlineno
     filename = method.func_code.co_filename
-    @wraps(method)
     def not_implemented(*args, **kwargs):
         raise NotImplementedError('Abstract method %s at File "%s", line %s '
                                    'should be implemented by a concrete class' %
                                    (repr(method), filename, line))
     return not_implemented
 
-class DeviceManager(object):
-    """
-    Represents a connection to a device. Once an implementation of this class
-    is successfully instantiated, you may do things like list/copy files to
-    the device, launch processes on the device, and install or remove
-    applications from the device.
-
-    Never instantiate this class directly! Instead, instantiate an
-    implementation of it like DeviceManagerADB or DeviceManagerSUT.
-    """
+class DeviceManager:
 
     _logcatNeedsRoot = True
 
     @abstractmethod
-    def getInfo(self, directive=None):
+    def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
+        """
+        Executes shell command on device and returns exit code
+
+        cmd - Command string to execute
+        outputfile - File to store output
+        env - Environment to pass to exec command
+        cwd - Directory to execute command from
+        timeout - specified in seconds, defaults to 'default_timeout'
+        root - Specifies whether command requires root privileges
+        """
+
+    def shellCheckOutput(self, cmd, env=None, cwd=None, timeout=None, root=False):
+        """
+        executes shell command on device and returns the the output
+
+        env - Environment to pass to exec command
+        cwd - Directory to execute command from
+        timeout - specified in seconds, defaults to 'default_timeout'
+        root - Specifies whether command requires root privileges
+        """
+        buf = StringIO.StringIO()
+        retval = self.shell(cmd, buf, env=env, cwd=cwd, timeout=timeout, root=root)
+        output = str(buf.getvalue()[0:-1]).rstrip()
+        buf.close()
+        if retval != 0:
+            raise DMError("Non-zero return code for command: %s (output: '%s', retval: '%s')" % (cmd, output, retval))
+        return output
+
+    @abstractmethod
+    def pushFile(self, localname, destname, retryLimit=1):
+        """
+        Copies localname from the host to destname on the device
+        """
+
+    @abstractmethod
+    def mkDir(self, name):
         """
-        Returns a dictionary of information strings about the device.
+        Creates a single directory on the device file system
+        """
+
+    def mkDirs(self, filename):
+        """
+        Make directory structure on the device
+        WARNING: does not create last part of the path
+        """
+        dirParts = filename.rsplit('/', 1)
+        if not self.dirExists(dirParts[0]):
+            parts = filename.split('/')
+            name = ""
+            for part in parts:
+                if part == parts[-1]:
+                    break
+                if part != "":
+                    name += '/' + part
+                    self.mkDir(name) # mkDir will check previous existence
 
-        :param directive: information you want to get. Options are:
+    @abstractmethod
+    def pushDir(self, localDir, remoteDir, retryLimit=1):
+        """
+        Push localDir from host to remoteDir on the device
+        """
+
+    @abstractmethod
+    def fileExists(self, filepath):
+        """
+        Checks if filepath exists and is a file on the device file system
+
+        returns:
+          success: True
+          failure: False
+        """
+
+    @abstractmethod
+    def listFiles(self, rootdir):
+        """
+        Lists files on the device rootdir
 
-          - `os` - name of the os
-          - `id` - unique id of the device
-          - `uptime` - uptime of the device
-          - `uptimemillis` - uptime of the device in milliseconds (NOT supported on all implementations)
-          - `systime` - system time of the device
-          - `screen` - screen resolution
-          - `memory` - memory stats
-          - `process` - list of running processes (same as ps)
-          - `disk` - total, free, available bytes on disk
-          - `power` - power status (charge, battery temp)
-          - `temperature` - device temperature
+        returns:
+          success: array of filenames, ['file1', 'file2', ...]
+          failure: None
+        """
+
+    @abstractmethod
+    def removeFile(self, filename):
+        """
+        Removes filename from the device
+
+        returns:
+          success: output of telnet
+          failure: None
+        """
+
+    @abstractmethod
+    def removeDir(self, remoteDir):
+        """
+        Does a recursive delete of directory on the device: rm -Rf remoteDir
+
+        returns:
+          success: output of telnet
+          failure: None
+        """
+
+    @abstractmethod
+    def getProcessList(self):
+        """
+        Lists the running processes on the device
+
+        returns:
+          success: array of process tuples
+          failure: []
+        """
+
+    def processExist(self, appname):
+        """
+        Iterates process list and checks if pid exists
 
-         If `directive` is `None`, will return all available information
+        returns:
+          success: pid
+          failure: None
+        """
+        if not isinstance(appname, basestring):
+            raise TypeError("appname %s is not a string" % appname)
+
+        pid = None
+
+        #filter out extra spaces
+        parts = filter(lambda x: x != '', appname.split(' '))
+        appname = ' '.join(parts)
+
+        #filter out the quoted env string if it exists
+        #ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
+        parts = appname.split('"')
+        if (len(parts) > 2):
+            appname = ' '.join(parts[2:]).strip()
+
+        pieces = appname.split(' ')
+        parts = pieces[0].split('/')
+        app = parts[-1]
+
+        procList = self.getProcessList()
+        if (procList == []):
+            return None
+
+        for proc in procList:
+            procName = proc[1].split('/')[-1]
+            if (procName == app):
+                pid = proc[0]
+                break
+        return pid
+
+
+    @abstractmethod
+    def killProcess(self, appname, forceKill=False):
+        """
+        Kills the process named appname.
+        If forceKill is True, process is killed regardless of state
+
+        returns:
+          success: True
+          failure: False
         """
 
     @abstractmethod
-    def getCurrentTime(self):
+    def catFile(self, remoteFile):
+        """
+        Returns the contents of remoteFile
+
+        returns:
+          success: filecontents, string
+          failure: None
+        """
+
+    @abstractmethod
+    def pullFile(self, remoteFile):
+        """
+        Returns contents of remoteFile using the "pull" command.
+
+        returns:
+          success: output of pullfile, string
+          failure: None
+        """
+
+    @abstractmethod
+    def getFile(self, remoteFile, localFile):
+        """
+        Copy file from device (remoteFile) to host (localFile)
+        """
+
+    @abstractmethod
+    def getDirectory(self, remoteDir, localDir, checkDir=True):
+        """
+        Copy directory structure from device (remoteDir) to host (localDir)
+
+        returns:
+          success: list of files, string
+          failure: None
+        """
+
+    @abstractmethod
+    def validateFile(self, remoteFile, localFile):
+        """
+        Checks if the remoteFile has the same md5 hash as the localFile
+
+        returns:
+          success: True
+          failure: False
+        """
+
+    @abstractmethod
+    def _getRemoteHash(self, filename):
+        """
+        Return the md5 sum of a file on the device
+
+        returns:
+          success: MD5 hash for given filename
+          failure: None
+        """
+
+    @staticmethod
+    def _getLocalHash(filename):
+        """
+        Return the MD5 sum of a file on the host
+
+        returns:
+          success: MD5 hash for given filename
+          failure: None
         """
-        Returns device time in milliseconds since the epoch.
+
+        f = open(filename, 'rb')
+        if (f == None):
+            return None
+
+        try:
+            mdsum = hashlib.md5()
+        except:
+            return None
+
+        while 1:
+            data = f.read(1024)
+            if not data:
+                break
+            mdsum.update(data)
+
+        f.close()
+        hexval = mdsum.hexdigest()
+        return hexval
+
+    @abstractmethod
+    def getDeviceRoot(self):
+        """
+        Gets the device root for the testing area on the device
+        For all devices we will use / type slashes and depend on the device-agent
+        to sort those out.  The agent will return us the device location where we
+        should store things, we will then create our /tests structure relative to
+        that returned path.
+        Structure on the device is as follows:
+        /tests
+            /<fennec>|<firefox>  --> approot
+            /profile
+            /xpcshell
+            /reftest
+            /mochitest
+
+        returns:
+          success: path for device root
+          failure: None
+        """
+
+    @abstractmethod
+    def getAppRoot(self, packageName=None):
         """
+        Returns the app root directory
+        E.g /tests/fennec or /tests/firefox
+
+        returns:
+          success: path for app root
+          failure: None
+        """
+        # TODO Support org.mozilla.firefox and B2G
+
+    def getTestRoot(self, harness):
+        """
+        Gets the directory location on the device for a specific test type
+        Harness is one of: xpcshell|reftest|mochitest
+
+        returns:
+          success: path for test root
+          failure: None
+        """
+
+        devroot = self.getDeviceRoot()
+        if (devroot == None):
+            return None
+
+        if (re.search('xpcshell', harness, re.I)):
+            self.testRoot = devroot + '/xpcshell'
+        elif (re.search('?(i)reftest', harness)):
+            self.testRoot = devroot + '/reftest'
+        elif (re.search('?(i)mochitest', harness)):
+            self.testRoot = devroot + '/mochitest'
+        return self.testRoot
+
+    @abstractmethod
+    def getTempDir(self):
+        """
+        Gets the temporary directory we are using on this device
+        base on our device root, ensuring also that it exists.
+
+        returns:
+          success: path for temporary directory
+          failure: None
+        """
+
+    def signal(self, processID, signalType, signalAction):
+        """
+        Sends a specific process ID a signal code and action.
+        For Example: SIGINT and SIGDFL to process x
+        """
+        #currently not implemented in device agent - todo
+        pass
+
+    def getReturnCode(self, processID):
+        """Get a return code from process ending -- needs support on device-agent"""
+        # TODO: make this real
+
+        return 0
 
     def getIP(self, interfaces=['eth0', 'wlan0']):
         """
-        Returns the IP of the device, or None if no connection exists.
+        Gets the IP of the device, or None if no connection exists.
         """
         for interface in interfaces:
             match = re.match(r"%s: ip (\S+)" % interface,
                              self.shellCheckOutput(['ifconfig', interface]))
             if match:
                 return match.group(1)
 
+    @abstractmethod
+    def unpackFile(self, file_path, dest_dir=None):
+        """
+        Unzips a remote bundle to a remote location
+        If dest_dir is not specified, the bundle is extracted
+        in the same directory
+
+        returns:
+          success: output of unzip command
+          failure: None
+        """
+
+    @abstractmethod
+    def reboot(self, ipAddr=None, port=30000):
+        """
+        Reboots the device
+
+        returns:
+          success: status from test agent
+          failure: None
+        """
+
+    def validateDir(self, localDir, remoteDir):
+        """
+        Validate localDir from host to remoteDir on the device
+
+        returns:
+          success: True
+          failure: False
+        """
+
+        if (self.debug >= 2):
+            print "validating directory: " + localDir + " to " + remoteDir
+        for root, dirs, files in os.walk(localDir):
+            parts = root.split(localDir)
+            for f in files:
+                remoteRoot = remoteDir + '/' + parts[1]
+                remoteRoot = remoteRoot.replace('/', '/')
+                if (parts[1] == ""):
+                    remoteRoot = remoteDir
+                remoteName = remoteRoot + '/' + f
+                if (self.validateFile(remoteName, os.path.join(root, f)) <> True):
+                        return False
+        return True
+
+    @abstractmethod
+    def getInfo(self, directive=None):
+        """
+        Returns information about the device:
+        Directive indicates the information you want to get, your choices are:
+          os - name of the os
+          id - unique id of the device
+          uptime - uptime of the device
+          uptimemillis - uptime of the device in milliseconds (NOT supported on all implementations)
+          systime - system time of the device
+          screen - screen resolution
+          memory - memory stats
+          process - list of running processes (same as ps)
+          disk - total, free, available bytes on disk
+          power - power status (charge, battery temp)
+          all - all of them - or call it with no parameters to get all the information
+
+        returns: dict of info strings by directive name
+        """
+
+    @abstractmethod
+    def installApp(self, appBundlePath, destPath=None):
+        """
+        Installs an application onto the device
+        appBundlePath - path to the application bundle on the device
+        destPath - destination directory of where application should be installed to (optional)
+        """
+
+    @abstractmethod
+    def uninstallApp(self, appName, installPath=None):
+        """
+        Uninstalls the named application from device and DOES NOT cause a reboot
+        appName - the name of the application (e.g org.mozilla.fennec)
+        installPath - the path to where the application was installed (optional)
+        """
+
+    @abstractmethod
+    def uninstallAppAndReboot(self, appName, installPath=None):
+        """
+        Uninstalls the named application from device and causes a reboot
+        appName - the name of the application (e.g org.mozilla.fennec)
+        installPath - the path to where the application was installed (optional)
+        """
+
+    @abstractmethod
+    def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
+        """
+        Updates the application on the device.
+        appBundlePath - path to the application bundle on the device
+        processName - used to end the process if the applicaiton is currently running (optional)
+        destPath - Destination directory to where the application should be installed (optional)
+        ipAddr - IP address to await a callback ping to let us know that the device has updated
+                 properly - defaults to current IP.
+        port - port to await a callback ping to let us know that the device has updated properly
+               defaults to 30000, and counts up from there if it finds a conflict
+        """
+
+    @abstractmethod
+    def getCurrentTime(self):
+        """
+        Returns device time in milliseconds since the epoch
+
+        returns:
+          success: time in ms
+          failure: None
+        """
+
     def recordLogcat(self):
         """
-        Clears the logcat file making it easier to view specific events.
+        Clears the logcat file making it easier to view specific events
         """
         #TODO: spawn this off in a separate thread/process so we can collect all the logcat information
 
         # Right now this is just clearing the logcat so we can only see what happens after this call.
         self.shellCheckOutput(['/system/bin/logcat', '-c'], root=self._logcatNeedsRoot)
 
-    def getLogcat(self, filterSpecs=["dalvikvm:I", "ConnectivityService:S",
+    def getLogcat(self, filterSpecs=["dalvikvm:S", "ConnectivityService:S",
                                       "WifiMonitor:S", "WifiStateTracker:S",
                                       "wpa_supplicant:S", "NetworkStateTracker:S"],
                   format="time",
                   filterOutRegexps=[]):
         """
         Returns the contents of the logcat file as a list of strings
         """
         cmdline = ["/system/bin/logcat", "-v", format, "-d"] + filterSpecs
         lines = self.shellCheckOutput(cmdline,
                                       root=self._logcatNeedsRoot).split('\r')
 
         for regex in filterOutRegexps:
             lines = [line for line in lines if not re.search(regex, line)]
 
         return lines
 
+    @staticmethod
+    def _writePNG(buf, width, height):
+        """
+        Method for writing a PNG from a buffer, used by getScreenshot on older devices
+        Based on: http://code.activestate.com/recipes/577443-write-a-png-image-in-native-python/
+        """
+        width_byte_4 = width * 4
+        raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in range(0, (height - 1) * width * 4, width_byte_4))
+        def png_pack(png_tag, data):
+            chunk_head = png_tag + data
+            return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+        return b"".join([
+                b'\x89PNG\r\n\x1a\n',
+                png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
+                png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+                png_pack(b'IEND', b'')])
+
     def saveScreenshot(self, filename):
         """
         Takes a screenshot of what's being display on the device. Uses
         "screencap" on newer (Android 3.0+) devices (and some older ones with
         the functionality backported). This function also works on B2G.
 
         Throws an exception on failure. This will always fail on devices
         without the screencap utility.
@@ -133,376 +552,28 @@ class DeviceManager(object):
             buf = self.pullFile(tempScreenshotFile)
             width = int(struct.unpack("I", buf[0:4])[0])
             height = int(struct.unpack("I", buf[4:8])[0])
             with open(filename, 'w') as pngfile:
                 pngfile.write(self._writePNG(buf[12:], width, height))
             self.removeFile(tempScreenshotFile)
 
     @abstractmethod
-    def pushFile(self, localFilename, remoteFilename, retryLimit=1):
-        """
-        Copies localname from the host to destname on the device.
-        """
-
-    @abstractmethod
-    def pushDir(self, localDirname, remoteDirname, retryLimit=1):
-        """
-        Push local directory from host to remote directory on the device,
-        """
-
-    @abstractmethod
-    def pullFile(self, remoteFilename):
-        """
-        Returns contents of remoteFile using the "pull" command.
-        """
-
-    @abstractmethod
-    def getFile(self, remoteFilename, localFilename):
-        """
-        Copy file from remote device to local file on host.
-        """
-
-    @abstractmethod
-    def getDirectory(self, remoteDirname, localDirname, checkDir=True):
-        """
-        Copy directory structure from device (remoteDirname) to host (localDirname).
-        """
-
-    @abstractmethod
-    def validateFile(self, remoteFilename, localFilename):
-        """
-        Returns True if a file on the remote device has the same md5 hash as a local one.
-        """
-
-    def validateDir(self, localDirname, remoteDirname):
-        """
-        Returns True if remoteDirname on device is same as localDirname on host.
-        """
-
-        if (self.debug >= 2):
-            print "validating directory: " + localDirname + " to " + remoteDirname
-        for root, dirs, files in os.walk(localDirname):
-            parts = root.split(localDirname)
-            for f in files:
-                remoteRoot = remoteDirname + '/' + parts[1]
-                remoteRoot = remoteRoot.replace('/', '/')
-                if (parts[1] == ""):
-                    remoteRoot = remoteDirname
-                remoteName = remoteRoot + '/' + f
-                if (self.validateFile(remoteName, os.path.join(root, f)) <> True):
-                        return False
-        return True
-
-    @abstractmethod
-    def mkDir(self, remoteDirname):
-        """
-        Creates a single directory on the device file system.
-        """
-
-    def mkDirs(self, filename):
-        """
-        Make directory structure on the device.
-
-        WARNING: does not create last part of the path. For example, if asked to
-        create `/mnt/sdcard/foo/bar/baz`, it will only create `/mnt/sdcard/foo/bar`
-        """
-        dirParts = filename.rsplit('/', 1)
-        if not self.dirExists(dirParts[0]):
-            parts = filename.split('/')
-            name = ""
-            for part in parts:
-                if part is parts[-1]:
-                    break
-                if part != "":
-                    name += '/' + part
-                    self.mkDir(name) # mkDir will check previous existence
-
-    @abstractmethod
-    def dirExists(self, dirpath):
-        """
-        Returns whether dirpath exists and is a directory on the device file system.
-        """
-
-    @abstractmethod
-    def fileExists(self, filepath):
-        """
-        Return whether filepath exists and is a file on the device file system.
-        """
-
-    @abstractmethod
-    def listFiles(self, rootdir):
-        """
-        Lists files on the device rootdir.
-
-        Returns array of filenames, ['file1', 'file2', ...]
-        """
-
-    @abstractmethod
-    def removeFile(self, filename):
-        """
-        Removes filename from the device.
-        """
-
-    @abstractmethod
-    def removeDir(self, remoteDirname):
-        """
-        Does a recursive delete of directory on the device: rm -Rf remoteDirname.
-        """
-
-    @abstractmethod
-    def chmodDir(self, remoteDirname, mask="777"):
-        """
-        Recursively changes file permissions in a directory.
-        """
-
-    @abstractmethod
-    def getDeviceRoot(self):
-        """
-        Gets the device root for the testing area on the device.
-
-        For all devices we will use / type slashes and depend on the device-agent
-        to sort those out.  The agent will return us the device location where we
-        should store things, we will then create our /tests structure relative to
-        that returned path.
-
-        Structure on the device is as follows:
-
-        ::
-
-          /tests
-              /<fennec>|<firefox>  --> approot
-              /profile
-              /xpcshell
-              /reftest
-              /mochitest
-        """
-
-    @abstractmethod
-    def getAppRoot(self, packageName=None):
-        """
-        Returns the app root directory.
-
-        E.g /tests/fennec or /tests/firefox
-        """
-        # TODO Support org.mozilla.firefox and B2G
-
-    def getTestRoot(self, harnessName):
-        """
-        Gets the directory location on the device for a specific test type.
-
-        :param harnessName: one of: "xpcshell", "reftest", "mochitest"
+    def chmodDir(self, remoteDir, mask="777"):
         """
-
-        devroot = self.getDeviceRoot()
-        if (devroot == None):
-            return None
-
-        if (re.search('xpcshell', harnessName, re.I)):
-            self.testRoot = devroot + '/xpcshell'
-        elif (re.search('?(i)reftest', harnessName)):
-            self.testRoot = devroot + '/reftest'
-        elif (re.search('?(i)mochitest', harnessName)):
-            self.testRoot = devroot + '/mochitest'
-        return self.testRoot
-
-    @abstractmethod
-    def getTempDir(self):
-        """
-        Returns a temporary directory we can use on this device, ensuring
-        also that it exists.
-        """
-
-    @abstractmethod
-    def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
-        """
-        Executes shell command on device and returns exit code.
-
-        :param cmd: Command string to execute
-        :param outputfile: File to store output
-        :param env: Environment to pass to exec command
-        :param cwd: Directory to execute command from
-        :param timeout: specified in seconds, defaults to 'default_timeout'
-        :param root: Specifies whether command requires root privileges
-        """
-
-    def shellCheckOutput(self, cmd, env=None, cwd=None, timeout=None, root=False):
-        """
-        Executes shell command on device and returns output as a string.
-
-        :param env: Environment to pass to exec command
-        :param cwd: Directory to execute command from
-        :param timeout: specified in seconds, defaults to 'default_timeout'
-        :param root: Specifies whether command requires root privileges
-        """
-        buf = StringIO.StringIO()
-        retval = self.shell(cmd, buf, env=env, cwd=cwd, timeout=timeout, root=root)
-        output = str(buf.getvalue()[0:-1]).rstrip()
-        buf.close()
-        if retval != 0:
-            raise DMError("Non-zero return code for command: %s (output: '%s', retval: '%s')" % (cmd, output, retval))
-        return output
-
-    @abstractmethod
-    def getProcessList(self):
-        """
-        Returns array of tuples representing running processes on the device.
-
-        Format of tuples is (processId, processName, userId)
-        """
-
-    def processExist(self, processName):
-        """
-        Returns True if process with name processName is running on device.
-        """
-        if not isinstance(processName, basestring):
-            raise TypeError("Process name %s is not a string" % processName)
-
-        pid = None
-
-        #filter out extra spaces
-        parts = filter(lambda x: x != '', processName.split(' '))
-        processName = ' '.join(parts)
-
-        #filter out the quoted env string if it exists
-        #ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
-        parts = processName.split('"')
-        if (len(parts) > 2):
-            processName = ' '.join(parts[2:]).strip()
+        Recursively changes file permissions in a directory
 
-        pieces = processName.split(' ')
-        parts = pieces[0].split('/')
-        app = parts[-1]
-
-        procList = self.getProcessList()
-        if (procList == []):
-            return None
-
-        for proc in procList:
-            procName = proc[1].split('/')[-1]
-            if (procName == app):
-                pid = proc[0]
-                break
-        return pid
-
-
-    @abstractmethod
-    def killProcess(self, processName, forceKill=False):
-        """
-        Kills the process named processName. If forceKill is True, process is
-        killed regardless of state.
-        """
-
-    @abstractmethod
-    def reboot(self, ipAddr=None, port=30000):
-        """
-        Reboots the device.
-
-        Some implementations may optionally support waiting for a TCP callback from
-        the device once it has restarted before returning, but this is not
-        guaranteed.
-        """
-
-    @abstractmethod
-    def installApp(self, appBundlePath, destPath=None):
-        """
-        Installs an application onto the device.
-
-        :param appBundlePath: path to the application bundle on the device
-        :param destPath: destination directory of where application should be installed to (optional)
-        """
-
-    @abstractmethod
-    def uninstallApp(self, appName, installPath=None):
-        """
-        Uninstalls the named application from device and DOES NOT cause a reboot.
-
-        :param appName: the name of the application (e.g org.mozilla.fennec)
-        :param installPath: the path to where the application was installed (optional)
-        """
-
-    @abstractmethod
-    def uninstallAppAndReboot(self, appName, installPath=None):
-        """
-        Uninstalls the named application from device and causes a reboot.
-
-        :param appName: the name of the application (e.g org.mozilla.fennec)
-        :param installPath: the path to where the application was installed (optional)
-        """
-
-    @abstractmethod
-    def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
-        """
-        Updates the application on the device.
-
-        :param appBundlePath: path to the application bundle on the device
-        :param processName: used to end the process if the applicaiton is
-                            currently running (optional)
-        :param destPath: Destination directory to where the application should
-                         be installed (optional)
-        :param ipAddr: IP address to await a callback ping to let us know that
-                       the device has updated properly (defaults to current
-                       IP)
-        :param port: port to await a callback ping to let us know that the
-                     device has updated properly defaults to 30000, and counts
-                     up from there if it finds a conflict
+        returns:
+          success: True
+          failure: False
         """
 
     @staticmethod
-    def _writePNG(buf, width, height):
-        """
-        Method for writing a PNG from a buffer, used by getScreenshot on older devices,
-        """
-        # Based on: http://code.activestate.com/recipes/577443-write-a-png-image-in-native-python/
-        width_byte_4 = width * 4
-        raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in range(0, (height - 1) * width * 4, width_byte_4))
-        def png_pack(png_tag, data):
-            chunk_head = png_tag + data
-            return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
-        return b"".join([
-                b'\x89PNG\r\n\x1a\n',
-                png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
-                png_pack(b'IDAT', zlib.compress(raw_data, 9)),
-                png_pack(b'IEND', b'')])
-
-    @abstractmethod
-    def _getRemoteHash(self, filename):
-        """
-        Return the md5 sum of a file on the device.
-        """
-
-    @staticmethod
-    def _getLocalHash(filename):
-        """
-        Return the MD5 sum of a file on the host.
-        """
-        f = open(filename, 'rb')
-        if (f == None):
-            return None
-
-        try:
-            mdsum = hashlib.md5()
-        except:
-            return None
-
-        while 1:
-            data = f.read(1024)
-            if not data:
-                break
-            mdsum.update(data)
-
-        f.close()
-        hexval = mdsum.hexdigest()
-        return hexval
-
-    @staticmethod
     def _escapedCommandLine(cmd):
-        """
-        Utility function to return escaped and quoted version of command line.
-        """
+        """ Utility function to return escaped and quoted version of command line """
         quotedCmd = []
 
         for arg in cmd:
             arg.replace('&', '\&')
 
             needsQuoting = False
             for char in [ ' ', '(', ')', '"', '&' ]:
                 if arg.find(char) >= 0:
@@ -562,17 +633,17 @@ class NetworkTools:
                 try:
                     s.bind((ip, seed))
                     connected = True
                     s.close()
                     break
                 except:
                     if seed > maxportnum:
                         print "Automation Error: Could not find open port after checking 5000 ports"
-                        raise
+                    raise
                 seed += 1
         except:
             print "Automation Error: Socket error trying to find open port"
 
         return seed
 
 def _pop_last_line(file_obj):
     """
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
@@ -6,34 +6,29 @@ import subprocess
 from devicemanager import DeviceManager, DMError, _pop_last_line
 import re
 import os
 import shutil
 import tempfile
 import time
 
 class DeviceManagerADB(DeviceManager):
-    """
-    Implementation of DeviceManager interface that uses the Android "adb"
-    utility to communicate with the device. Normally used to communicate
-    with a device that is directly connected with the host machine over a USB
-    port.
-    """
 
     _haveRootShell = False
     _haveSu = False
     _useRunAs = False
+    _useDDCopy = False
     _useZip = False
     _logcatNeedsRoot = False
     _pollingInterval = 0.01
     _packageName = None
     _tempDir = None
     default_timeout = 300
 
-    def __init__(self, host=None, port=5555, retryLimit=5, packageName='fennec',
+    def __init__(self, host=None, port=20701, retryLimit=5, packageName='fennec',
                  adbPath='adb', deviceSerial=None, deviceRoot=None, **kwargs):
         self.host = host
         self.port = port
         self.retryLimit = retryLimit
         self.deviceRoot = deviceRoot
 
         # the path to adb, or 'adb' to assume that it's on the PATH
         self._adbPath = adbPath
@@ -82,16 +77,26 @@ class DeviceManagerADB(DeviceManager):
         except DMError:
             pass
 
     def __del__(self):
         if self.host:
             self._disconnectRemoteADB()
 
     def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
+        """
+        Executes shell command on device. Returns exit code.
+
+        cmd - Command string to execute
+        outputfile - File to store output
+        env - Environment to pass to exec command
+        cwd - Directory to execute command from
+        timeout - specified in seconds, defaults to 'default_timeout'
+        root - Specifies whether command requires root privileges
+        """
         # FIXME: this function buffers all output of the command into memory,
         # always. :(
 
         # If requested to run as root, check that we can actually do that
         if root and not self._haveRootShell and not self._haveSu:
             raise DMError("Shell command '%s' requested to run as root but root "
                           "is not available on this device. Root your device or "
                           "refactor the test/harness to not require root." %
@@ -155,41 +160,53 @@ class DeviceManagerADB(DeviceManager):
 
     def _connectRemoteADB(self):
         self._checkCmd(["connect", self.host + ":" + str(self.port)])
 
     def _disconnectRemoteADB(self):
         self._checkCmd(["disconnect", self.host + ":" + str(self.port)])
 
     def pushFile(self, localname, destname, retryLimit=None):
+        """
+        Copies localname from the host to destname on the device
+        """
         # you might expect us to put the file *in* the directory in this case,
         # but that would be different behaviour from devicemanagerSUT. Throw
         # an exception so we have the same behaviour between the two
         # implementations
         retryLimit = retryLimit or self.retryLimit
         if self.dirExists(destname):
             raise DMError("Attempted to push a file (%s) to a directory (%s)!" %
                           (localname, destname))
 
         if self._useRunAs:
             remoteTmpFile = self.getTempDir() + "/" + os.path.basename(localname)
             self._checkCmd(["push", os.path.realpath(localname), remoteTmpFile],
                     retryLimit=retryLimit)
-            self.shellCheckOutput(["dd", "if=" + remoteTmpFile, "of=" + destname])
+            if self._useDDCopy:
+                self.shellCheckOutput(["dd", "if=" + remoteTmpFile, "of=" + destname])
+            else:
+                self.shellCheckOutput(["cp", remoteTmpFile, destname])
             self.shellCheckOutput(["rm", remoteTmpFile])
         else:
             self._checkCmd(["push", os.path.realpath(localname), destname],
                     retryLimit=retryLimit)
 
     def mkDir(self, name):
+        """
+        Creates a single directory on the device file system
+        """
         result = self._runCmdAs(["shell", "mkdir", name]).stdout.read()
         if 'read-only file system' in result.lower():
             raise DMError("Error creating directory: read only file system")
 
     def pushDir(self, localDir, remoteDir, retryLimit=None):
+        """
+        Push localDir from host to remoteDir on the device
+        """
         # adb "push" accepts a directory as an argument, but if the directory
         # contains symbolic links, the links are pushed, rather than the linked
         # files; we either zip/unzip or re-copy the directory into a temporary
         # one to get around this limitation
         retryLimit = retryLimit or self.retryLimit
         if not self.dirExists(remoteDir):
             self.mkDirs(remoteDir+"/x")
         if self._useZip:
@@ -213,44 +230,61 @@ class DeviceManagerADB(DeviceManager):
             tmpDir = tempfile.mkdtemp()
             # copytree's target dir must not already exist, so create a subdir
             tmpDirTarget = os.path.join(tmpDir, "tmp")
             shutil.copytree(localDir, tmpDirTarget)
             self._checkCmd(["push", tmpDirTarget, remoteDir], retryLimit=retryLimit)
             shutil.rmtree(tmpDir)
 
     def dirExists(self, remotePath):
+        """
+        Return True if remotePath is an existing directory on the device.
+        """
         p = self._runCmd(["shell", "ls", "-a", remotePath + '/'])
 
         data = p.stdout.readlines()
         if len(data) == 1:
             res = data[0]
             if "Not a directory" in res or "No such file or directory" in res:
                 return False
         return True
 
     def fileExists(self, filepath):
+        """
+        Return True if filepath exists and is a file on the device file system
+        """
         p = self._runCmd(["shell", "ls", "-a", filepath])
         data = p.stdout.readlines()
         if (len(data) == 1):
             if (data[0].rstrip() == filepath):
                 return True
         return False
 
     def removeFile(self, filename):
+        """
+        Removes filename from the device
+        """
         if self.fileExists(filename):
             self._runCmd(["shell", "rm", filename])
 
     def removeDir(self, remoteDir):
+        """
+        Does a recursive delete of directory on the device: rm -Rf remoteDir
+        """
         if (self.dirExists(remoteDir)):
             self._runCmd(["shell", "rm", "-r", remoteDir]).wait()
         else:
             self.removeFile(remoteDir.strip())
 
     def listFiles(self, rootdir):
+        """
+        Lists files on the device rootdir
+
+        returns array of filenames, ['file1', 'file2', ...]
+        """
         p = self._runCmd(["shell", "ls", "-a", rootdir])
         data = p.stdout.readlines()
         data[:] = [item.rstrip('\r\n') for item in data]
         if (len(data) == 1):
             if (data[0] == rootdir):
                 return []
             if (data[0].find("No such file or directory") != -1):
                 return []
@@ -258,28 +292,31 @@ class DeviceManagerADB(DeviceManager):
                 return []
             if (data[0].find("Permission denied") != -1):
                 return []
             if (data[0].find("opendir failed") != -1):
                 return []
         return data
 
     def getProcessList(self):
+        """
+        Lists the running processes on the device
+
+        returns:
+          success: array of process tuples
+          failure: []
+        """
         p = self._runCmd(["shell", "ps"])
             # first line is the headers
         p.stdout.readline()
         proc = p.stdout.readline()
         ret = []
         while (proc):
             els = proc.split()
-            # we need to figure out if this is "user pid name" or "pid user vsz stat command"
-            if els[1].isdigit():
-                ret.append(list([int(els[1]), els[len(els) - 1], els[0]]))
-            else:
-                ret.append(list([int(els[0]), els[len(els) - 1], els[1]]))
+            ret.append(list([int(els[1]), els[len(els) - 1], els[0]]))
             proc =  p.stdout.readline()
         return ret
 
     def fireProcess(self, appname, failIfRunning=False):
         """
         Starts a process
 
         returns: pid
@@ -335,29 +372,40 @@ class DeviceManagerADB(DeviceManager):
         if uri != "":
             acmd.append("-d")
             acmd.append(''.join(['\'',uri, '\'']));
         print acmd
         self._checkCmd(acmd)
         return outputFile
 
     def killProcess(self, appname, forceKill=False):
+        """
+        Kills the process named appname.
+
+        If forceKill is True, process is killed regardless of state
+        """
         procs = self.getProcessList()
         for (pid, name, user) in procs:
             if name == appname:
                 args = ["shell", "kill"]
                 if forceKill:
                     args.append("-9")
                 args.append(str(pid))
                 p = self._runCmdAs(args)
                 p.communicate()
                 if p.returncode != 0:
                     raise DMError("Error killing process "
                                   "'%s': %s" % (appname, p.stdout.read()))
 
+    def catFile(self, remoteFile):
+        """
+        Returns the contents of remoteFile
+        """
+        return self.pullFile(remoteFile)
+
     def _runPull(self, remoteFile, localFile):
         """
         Pulls remoteFile from device to host
         """
         try:
             # First attempt to pull file regularly
             outerr = self._runCmd(["pull",  remoteFile, localFile]).communicate()
 
@@ -376,33 +424,45 @@ class DeviceManagerADB(DeviceManager):
                         self._checkCmdAs(["shell", "chmod", "777", remoteTmpFile])
                         self._runCmd(["pull",  remoteTmpFile, localFile]).stdout.read()
                         # Clean up temporary file
                         self._checkCmdAs(["shell", "rm", remoteTmpFile])
         except (OSError, ValueError):
             raise DMError("Error pulling remote file '%s' to '%s'" % (remoteFile, localFile))
 
     def pullFile(self, remoteFile):
+        """
+        Returns contents of remoteFile using the "pull" command.
+        """
         # TODO: add debug flags and allow for printing stdout
         localFile = tempfile.mkstemp()[1]
         self._runPull(remoteFile, localFile)
 
         f = open(localFile, 'r')
         ret = f.read()
         f.close()
         os.remove(localFile)
         return ret
 
     def getFile(self, remoteFile, localFile):
+        """
+        Copy file from device (remoteFile) to host (localFile).
+        """
         self._runPull(remoteFile, localFile)
 
     def getDirectory(self, remoteDir, localDir, checkDir=True):
-        self._runCmd(["pull", remoteDir, localDir]).wait()
+        """
+        Copy directory structure from device (remoteDir) to host (localDir)
+        """
+        self._runCmd(["pull", remoteDir, localDir])
 
     def validateFile(self, remoteFile, localFile):
+        """
+        Returns True if remoteFile has the same md5 hash as the localFile
+        """
         md5Remote = self._getRemoteHash(remoteFile)
         md5Local = self._getLocalHash(localFile)
         if md5Remote is None or md5Local is None:
             return None
         return md5Remote == md5Local
 
     def _getRemoteHash(self, remoteFile):
         """
@@ -428,75 +488,142 @@ class DeviceManagerADB(DeviceManager):
             if not self.dirExists(self.deviceRoot):
                 try:
                     self.mkDir(self.deviceRoot)
                 except:
                     print "Unable to create device root %s" % self.deviceRoot
                     raise
             return
 
+        # /mnt/sdcard/tests is preferred to /data/local/tests, but this can be
+        # over-ridden by creating /data/local/tests
+        testRoot = "/data/local/tests"
+        if (self.dirExists(testRoot)):
+            self.deviceRoot = testRoot
+            return
+
         paths = [('/mnt/sdcard', 'tests'),
                  ('/data/local', 'tests')]
         for (basePath, subPath) in paths:
             if self.dirExists(basePath):
                 testRoot = os.path.join(basePath, subPath)
                 try:
                     self.mkDir(testRoot)
                     self.deviceRoot = testRoot
                     return
                 except:
                     pass
 
         raise DMError("Unable to set up device root using paths: [%s]"
                         % ", ".join(["'%s'" % os.path.join(b, s) for b, s in paths]))
 
     def getDeviceRoot(self):
+        """
+        Gets the device root for the testing area on the device
+
+        For all devices we will use / type slashes and depend on the device-agent
+        to sort those out.  The agent will return us the device location where we
+        should store things, we will then create our /tests structure relative to
+        that returned path.
+        Structure on the device is as follows:
+        /tests
+            /<fennec>|<firefox>  --> approot
+            /profile
+            /xpcshell
+            /reftest
+            /mochitest
+        """
         return self.deviceRoot
 
     def getTempDir(self):
+        """
+        Return a temporary directory on the device
+
+        Will also ensure that directory exists
+        """
         # Cache result to speed up operations depending
         # on the temporary directory.
         if not self._tempDir:
             self._tempDir = self.getDeviceRoot() + "/tmp"
             self.mkDir(self._tempDir)
 
         return self._tempDir
 
     def getAppRoot(self, packageName):
+        """
+        Returns the app root directory
+
+        E.g /tests/fennec or /tests/firefox
+        """
         devroot = self.getDeviceRoot()
         if (devroot == None):
             return None
 
         if (packageName and self.dirExists('/data/data/' + packageName)):
             self._packageName = packageName
             return '/data/data/' + packageName
         elif (self._packageName and self.dirExists('/data/data/' + self._packageName)):
             return '/data/data/' + self._packageName
 
         # Failure (either not installed or not a recognized platform)
         raise DMError("Failed to get application root for: %s" % packageName)
 
     def reboot(self, wait = False, **kwargs):
+        """
+        Reboots the device
+        """
         self._runCmd(["reboot"])
         if (not wait):
             return
         countdown = 40
         while (countdown > 0):
             self._checkCmd(["wait-for-device", "shell", "ls", "/sbin"])
 
     def updateApp(self, appBundlePath, **kwargs):
+        """
+        Updates the application on the device.
+
+        appBundlePath - path to the application bundle on the device
+        processName - used to end the process if the applicaiton is currently running (optional)
+        destPath - Destination directory to where the application should be installed (optional)
+        ipAddr - IP address to await a callback ping to let us know that the device has updated
+                 properly - defaults to current IP.
+        port - port to await a callback ping to let us know that the device has updated properly
+               defaults to 30000, and counts up from there if it finds a conflict
+        """
         return self._runCmd(["install", "-r", appBundlePath]).stdout.read()
 
     def getCurrentTime(self):
+        """
+        Returns device time in milliseconds since the epoch
+        """
         timestr = self._runCmd(["shell", "date", "+%s"]).stdout.read().strip()
         if (not timestr or not timestr.isdigit()):
             raise DMError("Unable to get current time using date (got: '%s')" % timestr)
         return str(int(timestr)*1000)
 
     def getInfo(self, directive=None):
+        """
+        Returns information about the device
+
+        Directive indicates the information you want to get, your choices are:
+          os - name of the os
+          id - unique id of the device
+          uptime - uptime of the device
+          uptimemillis - uptime of the device in milliseconds (NOT supported on all implementations)
+          systime - system time of the device
+          screen - screen resolution
+          memory - memory stats
+          process - list of running processes (same as ps)
+          disk - total, free, available bytes on disk
+          power - power status (charge, battery temp)
+          all - all of them - or call it with no parameters to get all the information
+
+        returns: dictionary of info strings by directive name
+        """
         ret = {}
         if (directive == "id" or directive == "all"):
             ret["id"] = self._runCmd(["get-serialno"]).stdout.read()
         if (directive == "os" or directive == "all"):
             ret["os"] = self._runCmd(["shell", "getprop", "ro.build.display.id"]).stdout.read()
         if (directive == "uptime" or directive == "all"):
             utime = self._runCmd(["shell", "uptime"]).stdout.read()
             if (not utime):
@@ -511,22 +638,34 @@ class DeviceManagerADB(DeviceManager):
         if (directive == "process" or directive == "all"):
             ret["process"] = self._runCmd(["shell", "ps"]).stdout.read()
         if (directive == "systime" or directive == "all"):
             ret["systime"] = self._runCmd(["shell", "date"]).stdout.read()
         print ret
         return ret
 
     def uninstallApp(self, appName, installPath=None):
+        """
+        Uninstalls the named application from device and DOES NOT cause a reboot
+
+        appName - the name of the application (e.g org.mozilla.fennec)
+        installPath - the path to where the application was installed (optional)
+        """
         data = self._runCmd(["uninstall", appName]).stdout.read().strip()
         status = data.split('\n')[0].strip()
         if status != 'Success':
             raise DMError("uninstall failed for %s. Got: %s" % (appName, status))
 
     def uninstallAppAndReboot(self, appName, installPath=None):
+        """
+        Uninstalls the named application from device and causes a reboot
+
+        appName - the name of the application (e.g org.mozilla.fennec)
+        installPath - the path to where the application was installed (optional)
+        """
         self.uninstallApp(appName)
         self.reboot()
         return
 
     def _runCmd(self, args):
         """
         Runs a command using adb
 
@@ -578,17 +717,17 @@ class DeviceManagerADB(DeviceManager):
         finalArgs.extend(args)
         if not timeout:
             # We are asserting that all commands will complete in this time unless otherwise specified
             timeout = self.default_timeout
 
         timeout = int(timeout)
         retries = 0
         while retries < retryLimit:
-            proc = subprocess.Popen(finalArgs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+            proc = subprocess.Popen(finalArgs)
             start_time = time.time()
             ret_code = proc.poll()
             while ((time.time() - start_time) <= timeout) and ret_code == None:
                 time.sleep(self._pollingInterval)
                 ret_code = proc.poll()
             if ret_code == None:
                 proc.kill()
                 retries += 1
@@ -606,16 +745,19 @@ class DeviceManagerADB(DeviceManager):
         """
         retryLimit = retryLimit or self.retryLimit
         if (self._useRunAs):
             args.insert(1, "run-as")
             args.insert(2, self._packageName)
         return self._checkCmd(args, timeout, retryLimit=retryLimit)
 
     def chmodDir(self, remoteDir, mask="777"):
+        """
+        Recursively changes file permissions in a directory
+        """
         if (self.dirExists(remoteDir)):
             files = self.listFiles(remoteDir.strip())
             for f in files:
                 remoteEntry = remoteDir.strip() + "/" + f.strip()
                 if (self.dirExists(remoteEntry)):
                     self.chmodDir(remoteEntry)
                 else:
                     self._checkCmdAs(["shell", "chmod", mask, remoteEntry])
@@ -627,17 +769,17 @@ class DeviceManagerADB(DeviceManager):
             print "chmod " + remoteDir.strip()
 
     def _verifyADB(self):
         """
         Check to see if adb itself can be executed.
         """
         if self._adbPath != 'adb':
             if not os.access(self._adbPath, os.X_OK):
-                raise DMError("invalid adb path, or adb not executable: %s" % self._adbPath)
+                raise DMError("invalid adb path, or adb not executable: %s", self._adbPath)
 
         try:
             self._checkCmd(["version"])
         except os.error, err:
             raise DMError("unable to execute ADB (%s): ensure Android SDK is installed and adb is in your $PATH" % err)
         except subprocess.CalledProcessError:
             raise DMError("unable to execute ADB: ensure Android SDK is installed and adb is in your $PATH")
 
@@ -654,46 +796,64 @@ class DeviceManagerADB(DeviceManager):
                     if self._deviceSerial == m.group(1):
                         deviceStatus = m.group(2)
             if deviceStatus == None:
                 raise DMError("device not found: %s" % self._deviceSerial)
             elif deviceStatus != "device":
                 raise DMError("bad status for device %s: %s" % (self._deviceSerial, deviceStatus))
 
         # Check to see if we can connect to device and run a simple command
-        ret = None
         try:
-            ret = self._checkCmd(["shell", "echo"])
+            self._checkCmd(["shell", "echo"])
         except subprocess.CalledProcessError:
             raise DMError("unable to connect to device: is it plugged in?")
-        if ret:
-            raise DMError("unable to connect to device")
+
+    def _isCpAvailable(self):
+        """
+        Checks to see if cp command is installed
+        """
+        # Some Android systems may not have a cp command installed,
+        # or it may not be executable by the user.
+        data = self._runCmd(["shell", "cp"]).stdout.read()
+        if (re.search('Usage', data)):
+            return True
+        else:
+            data = self._runCmd(["shell", "dd", "-"]).stdout.read()
+            if (re.search('unknown operand', data)):
+                print "'cp' not found, but 'dd' was found as a replacement"
+                self._useDDCopy = True
+                return True
+            print "unable to execute 'cp' on device; consider installing busybox from Android Market"
+            return False
 
     def _verifyRunAs(self):
         # If a valid package name is available, and certain other
         # conditions are met, devicemanagerADB can execute file operations
         # via the "run-as" command, so that pushed files and directories
         # are created by the uid associated with the package, more closely
         # echoing conditions encountered by Fennec at run time.
         # Check to see if run-as can be used here, by verifying a
         # file copy via run-as.
         self._useRunAs = False
         devroot = self.getDeviceRoot()
-        if self._packageName and devroot:
+        if (self._packageName and self._isCpAvailable() and devroot):
             tmpDir = self.getTempDir()
 
             # The problem here is that run-as doesn't cause a non-zero exit code
             # when failing because of a non-existent or non-debuggable package :(
             runAsOut = self._runCmd(["shell", "run-as", self._packageName, "mkdir", devroot + "/sanity"]).communicate()[0]
             if runAsOut.startswith("run-as:") and ("not debuggable" in runAsOut or "is unknown" in runAsOut):
                 raise DMError("run-as failed sanity check")
 
             tmpfile = tempfile.NamedTemporaryFile()
             self._checkCmd(["push", tmpfile.name, tmpDir + "/tmpfile"])
-            self._checkCmd(["shell", "run-as", self._packageName, "dd", "if=" + tmpDir + "/tmpfile", "of=" + devroot + "/sanity/tmpfile"])
+            if self._useDDCopy:
+                self._checkCmd(["shell", "run-as", self._packageName, "dd", "if=" + tmpDir + "/tmpfile", "of=" + devroot + "/sanity/tmpfile"])
+            else:
+                self._checkCmd(["shell", "run-as", self._packageName, "cp", tmpDir + "/tmpfile", devroot + "/sanity"])
             if (self.fileExists(devroot + "/sanity/tmpfile")):
                 print "will execute commands via run-as " + self._packageName
                 self._useRunAs = True
             self._checkCmd(["shell", "rm", devroot + "/tmp/tmpfile"])
             self._checkCmd(["shell", "run-as", self._packageName, "rm", "-r", devroot + "/sanity"])
 
     def _checkForRoot(self):
         # Check whether we _are_ root by default (some development boards work
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
@@ -1,59 +1,49 @@
 # 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 select
 import socket
+import SocketServer
 import time
 import os
 import re
 import posixpath
 import subprocess
+from threading import Thread
 import StringIO
 from devicemanager import DeviceManager, DMError, NetworkTools, _pop_last_line
 import errno
 from distutils.version import StrictVersion
 
 class DeviceManagerSUT(DeviceManager):
-    """
-    Implementation of DeviceManager interface that speaks to a device over
-    TCP/IP using the "system under test" protocol. A software agent such as
-    Negatus (http://github.com/mozilla/Negatus) or the Mozilla Android SUTAgent
-    app must be present and listening for connections for this to work.
-    """
-
     debug = 2
     _base_prompt = '$>'
     _base_prompt_re = '\$\>'
     _prompt_sep = '\x00'
     _prompt_regex = '.*(' + _base_prompt_re + _prompt_sep + ')'
     _agentErrorRE = re.compile('^##AGENT-WARNING##\ ?(.*)')
     default_timeout = 300
 
-    reboot_timeout = 600
-    reboot_settling_time = 60
-
     def __init__(self, host, port = 20701, retryLimit = 5, deviceRoot = None, **kwargs):
         self.host = host
         self.port = port
         self.retryLimit = retryLimit
         self._sock = None
         self._everConnected = False
         self.deviceRoot = deviceRoot
 
         # Initialize device root
         self.getDeviceRoot()
 
         # Get version
         verstring = self._runCmds([{ 'cmd': 'ver' }])
-        ver_re = re.match('(\S+) Version (\S+)', verstring)
-        self.agentProductName = ver_re.group(1)
-        self.agentVersion = ver_re.group(2)
+        self.agentVersion = re.sub('SUTAgentAndroid Version ', '', verstring)
 
     def _cmdNeedsResponse(self, cmd):
         """ Not all commands need a response from the agent:
             * rebt obviously doesn't get a response
             * uninstall performs a reboot to ensure starting in a clean state and
               so also doesn't look for a response
         """
         noResponseCmds = [re.compile('^rebt'),
@@ -165,33 +155,27 @@ class DeviceManagerSUT(DeviceManager):
                 if self.debug >= 1 and self._everConnected:
                     print "reconnecting socket"
                 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
             except socket.error, msg:
                 self._sock = None
                 raise DMError("Automation Error: unable to create socket: "+str(msg))
 
             try:
-                self._sock.settimeout(float(timeout))
                 self._sock.connect((self.host, int(self.port)))
+                if select.select([self._sock], [], [], timeout)[0]:
+                    self._sock.recv(1024)
+                else:
+                    raise DMError("Remote Device Error: Timeout in connecting", fatal=True)
+                    return False
                 self._everConnected = True
             except socket.error, msg:
-                self._sock = None
-                raise DMError("Remote Device Error: Unable to connect socket: "+str(msg))
-
-            # consume prompt
-            try:
-                self._sock.recv(1024)
-            except socket.error, msg:
                 self._sock.close()
                 self._sock = None
-                raise DMError("Remote Device Error: Did not get prompt after connecting: " + str(msg), fatal=True)
-
-            # future recv() timeouts are handled by select() calls
-            self._sock.settimeout(None)
+                raise DMError("Remote Device Error: Unable to connect socket: "+str(msg))
 
         for cmd in cmdlist:
             cmdline = '%s\r\n' % cmd['cmd']
 
             try:
                 sent = self._sock.send(cmdline)
                 if sent != len(cmdline):
                     raise DMError("Remote Device Error: our cmd was %s bytes and we "
@@ -289,26 +273,31 @@ class DeviceManagerSUT(DeviceManager):
             try:
                 self._sock.close()
                 self._sock = None
             except:
                 self._sock = None
                 raise DMError("Automation Error: Error closing socket")
 
     def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
+        """
+        Executes shell command on device. Returns exit code.
+
+        cmd - Command string to execute
+        outputfile - File to store output
+        env - Environment to pass to exec command
+        cwd - Directory to execute command from
+        timeout - specified in seconds, defaults to 'default_timeout'
+        root - Specifies whether command requires root privileges
+        """
         cmdline = self._escapedCommandLine(cmd)
         if env:
             cmdline = '%s %s' % (self._formatEnvString(env), cmdline)
 
-        # execcwd/execcwdsu currently unsupported in Negatus; see bug 824127.
-        if cwd and self.agentProductName == 'SUTAgentNegatus':
-            raise DMError("Negatus does not support execcwd/execcwdsu")
-
-        haveExecSu = (self.agentProductName == 'SUTAgentNegatus' or
-                      StrictVersion(self.agentVersion) >= StrictVersion('1.13'))
+        haveExecSu = (StrictVersion(self.agentVersion) >= StrictVersion('1.13'))
 
         # Depending on agent version we send one of the following commands here:
         # * exec (run as normal user)
         # * execsu (run as privileged user)
         # * execcwd (run as normal user from specified directory)
         # * execcwdsu (run as privileged user from specified directory)
 
         cmd = "exec"
@@ -336,41 +325,50 @@ class DeviceManagerSUT(DeviceManager):
             m = re.search('return code \[([0-9]+)\]', lastline)
             if m:
                 return int(m.group(1))
 
         # woops, we couldn't find an end of line/return value
         raise DMError("Automation Error: Error finding end of line/return value when running '%s'" % cmdline)
 
     def pushFile(self, localname, destname, retryLimit = None):
+        """
+        Copies localname from the host to destname on the device
+        """
         retryLimit = retryLimit or self.retryLimit
         self.mkDirs(destname)
 
         try:
             filesize = os.path.getsize(localname)
             with open(localname, 'rb') as f:
                 remoteHash = self._runCmds([{ 'cmd': 'push ' + destname + ' ' + str(filesize),
                                               'data': f.read() }], retryLimit=retryLimit).strip()
         except OSError:
             raise DMError("DeviceManager: Error reading file to push")
 
         if (self.debug >= 3):
-            print "push returned: %s" % remoteHash
+            print "push returned: %s" % hash
 
         localHash = self._getLocalHash(localname)
 
         if localHash != remoteHash:
             raise DMError("Automation Error: Push File failed to Validate! (localhash: %s, "
                           "remotehash: %s)" % (localHash, remoteHash))
 
     def mkDir(self, name):
+        """
+        Creates a single directory on the device file system
+        """
         if not self.dirExists(name):
             self._runCmds([{ 'cmd': 'mkdr ' + name }])
 
     def pushDir(self, localDir, remoteDir, retryLimit = None):
+        """
+        Push localDir from host to remoteDir on the device
+        """
         retryLimit = retryLimit or self.retryLimit
         if (self.debug >= 2):
             print "pushing directory: %s to %s" % (localDir, remoteDir)
 
         existentDirectories = []
         for root, dirs, files in os.walk(localDir, followlinks=True):
             parts = root.split(localDir)
             for f in files:
@@ -387,52 +385,74 @@ class DeviceManagerSUT(DeviceManager):
                 if parent not in existentDirectories:
                     self.mkDirs(remoteName)
                     existentDirectories.append(parent)
 
                 self.pushFile(os.path.join(root, f), remoteName, retryLimit=retryLimit)
 
 
     def dirExists(self, remotePath):
+        """
+        Return True if remotePath is an existing directory on the device.
+        """
         ret = self._runCmds([{ 'cmd': 'isdir ' + remotePath }]).strip()
         if not ret:
             raise DMError('Automation Error: DeviceManager isdir returned null')
 
         return ret == 'TRUE'
 
     def fileExists(self, filepath):
+        """
+        Return True if filepath exists and is a file on the device file system
+        """
         # Because we always have / style paths we make this a lot easier with some
         # assumptions
         s = filepath.split('/')
         containingpath = '/'.join(s[:-1])
         return s[-1] in self.listFiles(containingpath)
 
     def listFiles(self, rootdir):
+        """
+        Lists files on the device rootdir
+
+        returns array of filenames, ['file1', 'file2', ...]
+        """
         rootdir = rootdir.rstrip('/')
         if (self.dirExists(rootdir) == False):
             return []
         data = self._runCmds([{ 'cmd': 'cd ' + rootdir }, { 'cmd': 'ls' }])
 
         files = filter(lambda x: x, data.splitlines())
         if len(files) == 1 and files[0] == '<empty>':
             # special case on the agent: empty directories return just the string "<empty>"
             return []
         return files
 
     def removeFile(self, filename):
+        """
+        Removes filename from the device
+        """
         if (self.debug>= 2):
             print "removing file: " + filename
         if self.fileExists(filename):
             self._runCmds([{ 'cmd': 'rm ' + filename }])
 
     def removeDir(self, remoteDir):
+        """
+        Does a recursive delete of directory on the device: rm -Rf remoteDir
+        """
         if self.dirExists(remoteDir):
             self._runCmds([{ 'cmd': 'rmdr ' + remoteDir }])
 
     def getProcessList(self):
+        """
+        Lists the running processes on the device
+
+        returns: array of process tuples
+        """
         data = self._runCmds([{ 'cmd': 'ps' }])
 
         processTuples = []
         for line in data.splitlines():
             if line:
                 pidproc = line.strip().split()
                 try:
                     if (len(pidproc) == 2):
@@ -445,17 +465,17 @@ class DeviceManagerSUT(DeviceManager):
                         raise ValueError
                 except ValueError:
                     print "ERROR: Unable to parse process list (bug 805969)"
                     print "Line: %s\nFull output of process list:\n%s" % (line, data)
                     raise DMError("Invalid process line: %s" % line)
 
         return processTuples
 
-    def fireProcess(self, appname, failIfRunning=False, maxWaitTime=30):
+    def fireProcess(self, appname, failIfRunning=False):
         """
         Starts a process
 
         returns: pid
 
         DEPRECATED: Use shell() or launchApplication() for new code
         """
         if not appname:
@@ -463,32 +483,21 @@ class DeviceManagerSUT(DeviceManager):
 
         if (self.debug >= 2):
             print "FIRE PROC: '" + appname + "'"
 
         if (self.processExist(appname) != None):
             print "WARNING: process %s appears to be running already\n" % appname
             if (failIfRunning):
                 raise DMError("Automation Error: Process is already running")
-
         self._runCmds([{ 'cmd': 'exec ' + appname }])
 
         # The 'exec' command may wait for the process to start and end, so checking
         # for the process here may result in process = None.
-        # The normal case is to launch the process and return right away
-        # There is one case with robotium (am instrument) where exec returns at the end
-        pid = None
-        waited = 0
-        while pid is None and waited < maxWaitTime:
-            pid = self.processExist(appname)
-            if pid:
-                break
-            time.sleep(1)
-            waited += 1
-
+        pid = self.processExist(appname)
         if (self.debug >= 4):
             print "got pid: %s for process: %s" % (pid, appname)
         return pid
 
     def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False):
         """
         Launches a process, redirecting output to standard out
 
@@ -515,37 +524,44 @@ class DeviceManagerSUT(DeviceManager):
         # Prepend our env to the command
         cmdline = '%s %s' % (self._formatEnvString(env), cmdline)
 
         # fireProcess may trigger an exception, but we won't handle it
         self.fireProcess(cmdline, failIfRunning)
         return outputFile
 
     def killProcess(self, appname, forceKill=False):
+        """
+        Kills the process named appname
+
+        If forceKill is True, process is killed regardless of state
+        """
         if forceKill:
             print "WARNING: killProcess(): forceKill parameter unsupported on SUT"
-        retries = 0
-        while retries < self.retryLimit:
-            try:
-                if self.processExist(appname):
-                    self._runCmds([{ 'cmd': 'kill ' + appname }])
-                return
-            except DMError, err:
-                retries +=1
-                print ("WARNING: try %d of %d failed to kill %s" %
-                       (retries, self.retryLimit, appname))
-                if self.debug >= 4:
-                    print err
-                if retries >= self.retryLimit:
-                    raise err
+        if self.processExist(appname):
+            self._runCmds([{ 'cmd': 'kill ' + appname }])
 
     def getTempDir(self):
+        """
+        Return a temporary directory on the device
+
+        Will also ensure that directory exists
+        """
         return self._runCmds([{ 'cmd': 'tmpd' }]).strip()
 
+    def catFile(self, remoteFile):
+        """
+        Returns the contents of remoteFile
+        """
+        return self._runCmds([{ 'cmd': 'cat ' + remoteFile }])
+
     def pullFile(self, remoteFile):
+        """
+        Returns contents of remoteFile using the "pull" command.
+        """
         # The "pull" command is different from other commands in that DeviceManager
         # has to read a certain number of bytes instead of just reading to the
         # next prompt.  This is more robust than the "cat" command, which will be
         # confused if the prompt string exists within the file being catted.
         # However it means we can't use the response-handling logic in sendCMD().
 
         def err(error_msg):
             err_str = 'DeviceManager: pull unsuccessful: %s' % error_msg
@@ -633,26 +649,32 @@ class DeviceManagerSUT(DeviceManager):
         total_to_recv = filesize + len(prompt)
         buf = read_exact(total_to_recv, buf, 'could not get all file data')
         if buf[-len(prompt):] != prompt:
             err('no prompt found after file data--DeviceManager may be out of sync with agent')
             return buf
         return buf[:-len(prompt)]
 
     def getFile(self, remoteFile, localFile):
+        """
+        Copy file from device (remoteFile) to host (localFile)
+        """
         data = self.pullFile(remoteFile)
 
         fhandle = open(localFile, 'wb')
         fhandle.write(data)
         fhandle.close()
         if not self.validateFile(remoteFile, localFile):
             raise DMError("Automation Error: Failed to validate file when downloading %s" %
                           remoteFile)
 
     def getDirectory(self, remoteDir, localDir, checkDir=True):
+        """
+        Copy directory structure from device (remoteDir) to host (localDir)
+        """
         if (self.debug >= 2):
             print "getting files in '" + remoteDir + "'"
         if checkDir and not self.dirExists(remoteDir):
             raise DMError("Automation Error: Error getting directory: %s not a directory" %
                           remoteDir)
 
         filelist = self.listFiles(remoteDir)
         if (self.debug >= 3):
@@ -666,134 +688,147 @@ class DeviceManagerSUT(DeviceManager):
             remotePath = remoteDir + '/' + f
             localPath = os.path.join(localDir, f)
             if self.dirExists(remotePath):
                 self.getDirectory(remotePath, localPath, False)
             else:
                 self.getFile(remotePath, localPath)
 
     def validateFile(self, remoteFile, localFile):
+        """
+        Returns True if remoteFile has the same md5 hash as the localFile
+        """
         remoteHash = self._getRemoteHash(remoteFile)
         localHash = self._getLocalHash(localFile)
 
         if (remoteHash == None):
             return False
 
         if (remoteHash == localHash):
             return True
 
         return False
 
     def _getRemoteHash(self, filename):
+        """
+        Return the md5 sum of a file on the device
+        """
         data = self._runCmds([{ 'cmd': 'hash ' + filename }]).strip()
         if self.debug >= 3:
             print "remote hash returned: '%s'" % data
         return data
 
     def getDeviceRoot(self):
+        """
+        Gets the device root for the testing area on the device
+
+        For all devices we will use / type slashes and depend on the device-agent
+        to sort those out.  The agent will return us the device location where we
+        should store things, we will then create our /tests structure relative to
+        that returned path.
+        Structure on the device is as follows:
+        /tests
+            /<fennec>|<firefox>  --> approot
+            /profile
+            /xpcshell
+            /reftest
+            /mochitest
+        """
         if not self.deviceRoot:
             data = self._runCmds([{ 'cmd': 'testroot' }])
             self.deviceRoot = data.strip() + '/tests'
 
         if not self.dirExists(self.deviceRoot):
             self.mkDir(self.deviceRoot)
 
         return self.deviceRoot
 
     def getAppRoot(self, packageName):
+        """
+        Returns the app root directory
+
+        E.g /tests/fennec or /tests/firefox
+        """
         data = self._runCmds([{ 'cmd': 'getapproot ' + packageName }])
 
         return data.strip()
 
-    def unpackFile(self, filePath, destDir=None):
+    def unpackFile(self, file_path, dest_dir=None):
         """
-        Unzips a bundle to a location on the device
+        Unzips a remote bundle to a remote location
 
-        If destDir is not specified, the bundle is extracted in the same directory
+        If dest_dir is not specified, the bundle is extracted
+        in the same directory
         """
         devroot = self.getDeviceRoot()
         if (devroot == None):
             return None
 
-        # if no destDir is passed in just set it to filePath's folder
-        if not destDir:
-            destDir = posixpath.dirname(filePath)
-
-        if destDir[-1] != '/':
-            destDir += '/'
-
-        self._runCmds([{ 'cmd': 'unzp %s %s' % (filePath, destDir)}])
+        # if no dest_dir is passed in just set it to file_path's folder
+        if not dest_dir:
+            dest_dir = posixpath.dirname(file_path)
 
-    def _wait_for_reboot(self, host, port):
-        if self.debug >= 3:
-            print 'Creating server with %s:%d' % (host, port)
-        timeout_expires = time.time() + self.reboot_timeout
-        conn = None
-        data = ''
-        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        s.settimeout(60.0)
-        s.bind((host, port))
-        s.listen(1)
-        while not data and time.time() < timeout_expires:
-            try:
-                if not conn:
-                    conn, _ = s.accept()
-                # Receiving any data is good enough.
-                data = conn.recv(1024)
-                if data:
-                    conn.sendall('OK')
-                conn.close()
-            except socket.timeout:
-                print '.'
-            except socket.error, e:
-                if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK:
-                    raise
-        if data:
-            # Sleep to ensure not only we are online, but all our services are
-            # also up.
-            time.sleep(self.reboot_settling_time)
-        else:
-            print 'Automation Error: Timed out waiting for reboot callback.'
-        s.close()
-        return data
+        if dest_dir[-1] != '/':
+            dest_dir += '/'
+
+        self._runCmds([{ 'cmd': 'unzp %s %s' % (file_path, dest_dir)}])
 
     def reboot(self, ipAddr=None, port=30000):
+        """
+        Reboots the device
+        """
         cmd = 'rebt'
 
-        if self.debug > 3:
+        if (self.debug > 3):
             print "INFO: sending rebt command"
 
-        if ipAddr is not None:
-            # The update.info command tells the SUTAgent to send a TCP message
-            # after restarting.
+        if (ipAddr is not None):
+        #create update.info file:
             destname = '/data/data/com.mozilla.SUTAgentAndroid/files/update.info'
             data = "%s,%s\rrebooting\r" % (ipAddr, port)
-            self._runCmds([{'cmd': 'push %s %s' % (destname, len(data)),
-                            'data': data}])
+            self._runCmds([{ 'cmd': 'push %s %s' % (destname, len(data)), 'data': data }])
 
             ip, port = self._getCallbackIpAndPort(ipAddr, port)
             cmd += " %s %s" % (ip, port)
+            # Set up our callback server
+            callbacksvr = callbackServer(ip, port, self.debug)
 
-        status = self._runCmds([{'cmd': cmd}])
+        status = self._runCmds([{ 'cmd': cmd }])
 
-        if ipAddr is not None:
-            status = self._wait_for_reboot(ipAddr, port)
+        if (ipAddr is not None):
+            status = callbacksvr.disconnect()
 
-        if self.debug > 3:
+        if (self.debug > 3):
             print "INFO: rebt- got status back: " + str(status)
 
     def getInfo(self, directive=None):
+        """
+        Returns information about the device
+
+        Directive indicates the information you want to get, your choices are:
+          os - name of the os
+          id - unique id of the device
+          uptime - uptime of the device
+          uptimemillis - uptime of the device in milliseconds (NOT supported on all implementations)
+          systime - system time of the device
+          screen - screen resolution
+          memory - memory stats
+          process - list of running processes (same as ps)
+          disk - total, free, available bytes on disk
+          power - power status (charge, battery temp)
+          all - all of them - or call it with no parameters to get all the information
+
+        returns: dictionary of info strings by directive name
+        """
         data = None
         result = {}
         collapseSpaces = re.compile('  +')
 
         directives = ['os','id','uptime','uptimemillis','systime','screen',
-                      'rotation','memory','process','disk','power','sutuserinfo',
-                      'temperature']
+                                    'rotation','memory','process','disk','power']
         if (directive in directives):
             directives = [directive]
 
         for d in directives:
             data = self._runCmds([{ 'cmd': 'info ' + d }])
 
             data = collapseSpaces.sub(' ', data)
             result[d] = data.split('\n')
@@ -810,78 +845,112 @@ class DeviceManagerSUT(DeviceManager):
                     proclist.append(l.split('\t'))
             result['process'] = proclist
 
         if (self.debug >= 3):
             print "results: " + str(result)
         return result
 
     def installApp(self, appBundlePath, destPath=None):
+        """
+        Installs an application onto the device
+
+        appBundlePath - path to the application bundle on the device
+        destPath - destination directory of where application should be installed to (optional)
+        """
         cmd = 'inst ' + appBundlePath
         if destPath:
             cmd += ' ' + destPath
 
         data = self._runCmds([{ 'cmd': cmd }])
 
         f = re.compile('Failure')
         for line in data.split():
             if (f.match(line)):
                 raise DMError("Remove Device Error: Error installing app. Error message: %s" % data)
 
     def uninstallApp(self, appName, installPath=None):
+        """
+        Uninstalls the named application from device and DOES NOT cause a reboot
+
+        appName - the name of the application (e.g org.mozilla.fennec)
+        installPath - the path to where the application was installed (optional)
+        """
         cmd = 'uninstall ' + appName
         if installPath:
             cmd += ' ' + installPath
         data = self._runCmds([{ 'cmd': cmd }])
 
         status = data.split('\n')[0].strip()
         if self.debug > 3:
             print "uninstallApp: '%s'" % status
         if status == 'Success':
             return
         raise DMError("Remote Device Error: uninstall failed for %s" % appName)
 
     def uninstallAppAndReboot(self, appName, installPath=None):
+        """
+        Uninstalls the named application from device and causes a reboot
+
+        appName - the name of the application (e.g org.mozilla.fennec)
+        installPath - the path to where the application was installed (optional)
+        """
         cmd = 'uninst ' + appName
         if installPath:
             cmd += ' ' + installPath
         data = self._runCmds([{ 'cmd': cmd }])
 
         if (self.debug > 3):
             print "uninstallAppAndReboot: " + str(data)
         return
 
     def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
+        """
+        Updates the application on the device.
+
+        appBundlePath - path to the application bundle on the device
+        processName - used to end the process if the applicaiton is currently running (optional)
+        destPath - Destination directory to where the application should be installed (optional)
+        ipAddr - IP address to await a callback ping to let us know that the device has updated
+                 properly - defaults to current IP.
+        port - port to await a callback ping to let us know that the device has updated properly
+               defaults to 30000, and counts up from there if it finds a conflict
+        """
         status = None
         cmd = 'updt '
-        if processName is None:
+        if (processName == None):
             # Then we pass '' for processName
             cmd += "'' " + appBundlePath
         else:
             cmd += processName + ' ' + appBundlePath
 
-        if destPath:
+        if (destPath):
             cmd += " " + destPath
 
-        if ipAddr is not None:
+        if (ipAddr is not None):
             ip, port = self._getCallbackIpAndPort(ipAddr, port)
             cmd += " %s %s" % (ip, port)
+            # Set up our callback server
+            callbacksvr = callbackServer(ip, port, self.debug)
 
-        if self.debug >= 3:
+        if (self.debug >= 3):
             print "INFO: updateApp using command: " + str(cmd)
 
-        status = self._runCmds([{'cmd': cmd}])
+        status = self._runCmds([{ 'cmd': cmd }])
 
         if ipAddr is not None:
-            status = self._wait_for_reboot(ip, port)
+            status = callbacksvr.disconnect()
 
-        if self.debug >= 3:
-            print "INFO: updateApp: got status back: %s" + str(status)
+        if (self.debug >= 3):
+            print "INFO: updateApp: got status back: " + str(status)
 
     def getCurrentTime(self):
+        """
+        Returns device time in milliseconds since the epoch
+        """
         return self._runCmds([{ 'cmd': 'clok' }]).strip()
 
     def _getCallbackIpAndPort(self, aIp, aPort):
         """
         Connect the ipaddress and port for a callback ping.
 
         Defaults to current IP address and ports starting at 30000.
         NOTE: the detection for current IP address only works on Linux!
@@ -910,17 +979,17 @@ class DeviceManagerSUT(DeviceManager):
         retVal = '"%s"' % ','.join(map(lambda x: '%s=%s' % (x[0], x[1]), env.iteritems()))
         if (retVal == '""'):
             return ''
 
         return retVal
 
     def adjustResolution(self, width=1680, height=1050, type='hdmi'):
         """
-        Adjust the screen resolution on the device, REBOOT REQUIRED
+        adjust the screen resolution on the device, REBOOT REQUIRED
 
         NOTE: this only works on a tegra ATM
 
         supported resolutions: 640x480, 800x600, 1024x768, 1152x864, 1200x1024, 1440x900, 1680x1050, 1920x1080
         """
         if self.getInfo('os')['os'][0].split()[0] != 'harmony-eng':
             if (self.debug >= 2):
                 print "WARNING: unable to adjust screen resolution on non Tegra device"
@@ -952,9 +1021,69 @@ class DeviceManagerSUT(DeviceManager):
 
         if (self.debug >= 3):
             print "INFO: adjusting screen resolution to %s, %s and rebooting" % (width, height)
 
         self._runCmds([{ 'cmd': "exec setprop persist.tegra.dpy%s.mode.width %s" % (screentype, width) }])
         self._runCmds([{ 'cmd': "exec setprop persist.tegra.dpy%s.mode.height %s" % (screentype, height) }])
 
     def chmodDir(self, remoteDir, **kwargs):
+        """
+        Recursively changes file permissions in a directory
+        """
         self._runCmds([{ 'cmd': "chmod "+remoteDir }])
+
+gCallbackData = ''
+
+class myServer(SocketServer.TCPServer):
+    allow_reuse_address = True
+
+class callbackServer():
+    def __init__(self, ip, port, debuglevel):
+        global gCallbackData
+        if (debuglevel >= 1):
+            print "DEBUG: gCallbackData is: %s on port: %s" % (gCallbackData, port)
+        gCallbackData = ''
+        self.ip = ip
+        self.port = port
+        self.connected = False
+        self.debug = debuglevel
+        if (self.debug >= 3):
+            print "Creating server with " + str(ip) + ":" + str(port)
+        self.server = myServer((ip, port), self.myhandler)
+        self.server_thread = Thread(target=self.server.serve_forever)
+        self.server_thread.setDaemon(True)
+        self.server_thread.start()
+
+    def disconnect(self, step = 60, timeout = 600):
+        t = 0
+        if (self.debug >= 3):
+            print "Calling disconnect on callback server"
+        while t < timeout:
+            if (gCallbackData):
+                # Got the data back
+                if (self.debug >= 3):
+                    print "Got data back from agent: " + str(gCallbackData)
+                break
+            else:
+                if (self.debug >= 0):
+                    print '.',
+            time.sleep(step)
+            t += step
+
+        try:
+            if (self.debug >= 3):
+                print "Shutting down server now"
+            self.server.shutdown()
+        except:
+            if (self.debug >= 1):
+                print "Automation Error: Unable to shutdown callback server - check for a connection on port: " + str(self.port)
+
+        #sleep 1 additional step to ensure not only we are online, but all our services are online
+        time.sleep(step)
+        return gCallbackData
+
+    class myhandler(SocketServer.BaseRequestHandler):
+        def handle(self):
+            global gCallbackData
+            gCallbackData = self.request.recv(1024)
+            #print "Callback Handler got data: " + str(gCallbackData)
+            self.request.send("OK")
--- a/testing/mozbase/mozdevice/mozdevice/dmcli.py
+++ b/testing/mozbase/mozdevice/mozdevice/dmcli.py
@@ -20,21 +20,16 @@ class DMCli(object):
     def __init__(self):
         # a value of None for 'max_args' means there is no limit to the number
         # of arguments.  'min_args' should always have an integer value >= 0.
         self.commands = { 'install': { 'function': self.install,
                                        'min_args': 1,
                                        'max_args': 1,
                                        'help_args': '<file>',
                                        'help': 'push this package file to the device and install it' },
-                          'uninstall': { 'function': lambda a: self.dm.uninstallApp(a),
-                                         'min_args': 1,
-                                         'max_args': 1,
-                                         'help_args': '<packagename>',
-                                         'help': 'uninstall the named app from the device' },
                           'killapp': { 'function': self.killapp,
                                        'min_args': 1,
                                        'max_args': 1,
                                        'help_args': '<process name>',
                                        'help': 'kills any processes with a particular name on device' },
                           'launchapp': { 'function': self.launchapp,
                                          'min_args': 4,
                                          'max_args': 4,
@@ -105,23 +100,17 @@ class DMCli(object):
                                     'help_args': '<remote>',
                                     'help': 'recursively remove directory from device'
                                 },
                           'screencap': { 'function': lambda f: self.dm.saveScreenshot(f),
                                           'min_args': 1,
                                           'max_args': 1,
                                           'help_args': '<png file>',
                                           'help': 'capture screenshot of device in action'
-                                          },
-                          'sutver': { 'function': self.sutver,
-                                      'min_args': 0,
-                                      'max_args': 0,
-                                      'help_args': '',
-                                      'help': 'SUTAgent\'s product name and version (SUT only)'
-                                   },
+                                          }
 
                           }
 
         usage = "usage: %prog [options] <command> [<args>]\n\ndevice commands:\n"
         usage += "\n".join([textwrap.fill("%s %s - %s" %
                                           (cmdname, cmd['help_args'],
                                            cmd['help']),
                                           initial_indent="  ",
@@ -287,22 +276,15 @@ class DMCli(object):
     def isdir(self, file):
         if self.dm.dirExists(file):
             print "TRUE"
             return 0
 
         print "FALSE"
         return errno.ENOTDIR
 
-    def sutver(self):
-        if self.options.dmtype == 'sut':
-            print '%s Version %s' % (self.dm.agentProductName,
-                                     self.dm.agentVersion)
-        else:
-            print 'Must use SUT transport to get SUT version.'
-
 def cli(args=sys.argv[1:]):
     # process the command line
     cli = DMCli()
     cli.run(args)
 
 if __name__ == '__main__':
     cli()
--- a/testing/mozbase/mozdevice/mozdevice/droid.py
+++ b/testing/mozbase/mozdevice/mozdevice/droid.py
@@ -1,46 +1,36 @@
 # 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 StringIO
-import re
 import threading
 
 from Zeroconf import Zeroconf, ServiceBrowser
 from devicemanager import ZeroconfListener, NetworkTools
 from devicemanagerADB import DeviceManagerADB
 from devicemanagerSUT import DeviceManagerSUT
-from devicemanager import DMError
 
 class DroidMixin(object):
     """Mixin to extend DeviceManager with Android-specific functionality"""
 
-    def _getExtraAmStartArgs(self):
-        return []
-
     def launchApplication(self, appName, activityName, intent, url=None,
                           extras=None):
         """
         Launches an Android application
-
-        :param appName: Name of application (e.g. `com.android.chrome`)
-        :param activityName: Name of activity to launch (e.g. `.Main`)
-        :param intent: Intent to launch application with
-        :param url: URL to open
-        :param extras: Dictionary of extra arguments to launch application with
+        returns:
+        success: True
+        failure: False
         """
         # only one instance of an application may be running at once
         if self.processExist(appName):
-            raise DMError("Only one instance of an application may be running "
-                          "at once")
+            return False
 
-        acmd = [ "am", "start" ] + self._getExtraAmStartArgs() + \
-            ["-W", "-n", "%s/%s" % (appName, activityName)]
+        acmd = [ "am", "start", "-W", "-n", "%s/%s" % (appName, activityName)]
 
         if intent:
             acmd.extend(["-a", intent])
 
         if extras:
             for (key, val) in extras.iteritems():
                 if type(val) is int:
                     extraTypeParam = "--ei"
@@ -50,77 +40,56 @@ class DroidMixin(object):
                     extraTypeParam = "--es"
                 acmd.extend([extraTypeParam, str(key), str(val)])
 
         if url:
             acmd.extend(["-d", url])
 
         # shell output not that interesting and debugging logs should already
         # show what's going on here... so just create an empty memory buffer
-        # and ignore (except on error)
+        # and ignore
         shellOutput = StringIO.StringIO()
         if self.shell(acmd, shellOutput) == 0:
-            return
+            return True
 
-        shellOutput.seek(0)
-        raise DMError("Unable to launch application (shell output: '%s')" % shellOutput.read())
+        return False
 
     def launchFennec(self, appName, intent="android.intent.action.VIEW",
-                     mozEnv=None, extraArgs=None, url=None):
+                                      mozEnv=None, extraArgs=None, url=None):
         """
         Convenience method to launch Fennec on Android with various debugging
         arguments
-
-        :param appName: Name of fennec application (e.g. `org.mozilla.fennec`)
-        :param intent: Intent to launch application with
-        :param mozEnv: Mozilla specific environment to pass into application
-        :param extraArgs: Extra arguments to be parsed by fennec
-        :param url: URL to open
+        WARNING: FIXME: This would go better in mozrunner. Please do not
+        use this method if you are not comfortable with it going away sometime
+        in the near future
+        returns:
+        success: True
+        failure: False
         """
         extras = {}
 
         if mozEnv:
             # mozEnv is expected to be a dictionary of environment variables: Fennec
             # itself will set them when launched
             for (envCnt, (envkey, envval)) in enumerate(mozEnv.iteritems()):
                 extras["env" + str(envCnt)] = envkey + "=" + envval
 
         # Additional command line arguments that fennec will read and use (e.g.
         # with a custom profile)
         if extraArgs:
             extras['args'] = " ".join(extraArgs)
 
-        self.launchApplication(appName, ".App", intent, url=url, extras=extras)
+        return self.launchApplication(appName, ".App", intent, url=url,
+                                                                    extras=extras)
 
 class DroidADB(DeviceManagerADB, DroidMixin):
     pass
 
 class DroidSUT(DeviceManagerSUT, DroidMixin):
-
-    def _getExtraAmStartArgs(self):
-        # in versions of android in jellybean and beyond, the agent may run as
-        # a different process than the one that started the app. In this case,
-        # we need to get back the original user serial number and then pass
-        # that to the 'am start' command line
-        if not hasattr(self, 'userSerial'):
-            infoDict = self.getInfo(directive="sutuserinfo")
-            if infoDict.get('sutuserinfo') and \
-                    len(infoDict['sutuserinfo']) > 0:
-               userSerialString = infoDict['sutuserinfo'][0]
-               # user serial always an integer, see: http://developer.android.com/reference/android/os/UserManager.html#getSerialNumberForUser%28android.os.UserHandle%29
-               m = re.match('User Serial:([0-9]+)', userSerialString)
-               if m:
-                   self.userSerial = m.group(1)
-               else:
-                   self.userSerial = None
-
-        if self.userSerial is not None:
-            return [ "--user", self.userSerial ]
-
-        return []
+    pass
 
 def DroidConnectByHWID(hwid, timeout=30, **kwargs):
     """Try to connect to the given device by waiting for it to show up using mDNS with the given timeout."""
     nt = NetworkTools()
     local_ip = nt.getLanIp()
 
     zc = Zeroconf(local_ip)
 
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/emulator.py
@@ -0,0 +1,310 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from abc import abstractmethod
+import datetime
+from mozprocess import ProcessHandlerMixin
+import multiprocessing
+import os
+import re
+import shutil
+import socket
+import subprocess
+from telnetlib import Telnet
+import tempfile
+import time
+
+from emulator_battery import EmulatorBattery
+
+
+class LogcatProc(ProcessHandlerMixin):
+    """Process handler for logcat which saves all output to a logfile.
+    """
+
+    def __init__(self, logfile, cmd, **kwargs):
+        self.logfile = logfile
+        kwargs.setdefault('processOutputLine', []).append(self.log_output)
+        ProcessHandlerMixin.__init__(self, cmd, **kwargs)
+
+    def log_output(self, line):
+        f = open(self.logfile, 'a')
+        f.write(line + "\n")
+        f.flush()
+
+
+class Emulator(object):
+
+    deviceRe = re.compile(r"^emulator-(\d+)(\s*)(.*)$")
+
+    def __init__(self, noWindow=False, logcat_dir=None, arch="x86",
+                 emulatorBinary=None, res='480x800', userdata=None,
+                 memory='512', partition_size='512'):
+        self.port = None
+        self._emulator_launched = False
+        self.proc = None
+        self.local_port = None
+        self.telnet = None
+        self._tmp_userdata = None
+        self._adb_started = False
+        self.logcat_dir = logcat_dir
+        self.logcat_proc = None
+        self.arch = arch
+        self.binary = emulatorBinary
+        self.memory = str(memory)
+        self.partition_size = str(partition_size)
+        self.res = res
+        self.battery = EmulatorBattery(self)
+        self.noWindow = noWindow
+        self.dataImg = userdata
+        self.copy_userdata = self.dataImg is None
+
+    def __del__(self):
+        if self.telnet:
+            self.telnet.write('exit\n')
+            self.telnet.read_all()
+
+    @property
+    def args(self):
+        qemuArgs = [self.binary,
+                    '-kernel', self.kernelImg,
+                    '-sysdir', self.sysDir,
+                    '-data', self.dataImg]
+        if self.noWindow:
+            qemuArgs.append('-no-window')
+        qemuArgs.extend(['-memory', self.memory,
+                         '-partition-size', self.partition_size,
+                         '-verbose',
+                         '-skin', self.res,
+                         '-gpu', 'on',
+                         '-qemu'] + self.tail_args)
+        return qemuArgs
+
+    @property
+    def is_running(self):
+        if self._emulator_launched:
+            return self.proc is not None and self.proc.poll() is None
+        else:
+            return self.port is not None
+
+    def check_for_crash(self):
+        """
+        Checks if the emulator has crashed or not.  Always returns False if
+        we've connected to an already-running emulator, since we can't track
+        the emulator's pid in that case.  Otherwise, returns True iff
+        self.proc is not None (meaning the emulator hasn't been explicitly
+        closed), and self.proc.poll() is also not None (meaning the emulator
+        process has terminated).
+        """
+        if (self._emulator_launched and self.proc is not None
+                                    and self.proc.poll() is not None):
+            return True
+        return False
+
+    def _default_adb(self):
+        adb = subprocess.Popen(['which', 'adb'],
+                               stdout=subprocess.PIPE,
+                               stderr=subprocess.STDOUT)
+        retcode = adb.wait()
+        if retcode == 0:
+            self.adb = adb.stdout.read().strip() # remove trailing newline
+        return retcode
+
+    def _check_for_adb(self):
+        if not os.path.exists(self.adb):
+            if self._default_adb() != 0:
+                raise Exception('adb not found!')
+
+    def _run_adb(self, args):
+        args.insert(0, self.adb)
+        adb = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        retcode = adb.wait()
+        if retcode:
+            raise Exception('adb terminated with exit code %d: %s'
+            % (retcode, adb.stdout.read()))
+        return adb.stdout.read()
+
+    def _get_telnet_response(self, command=None):
+        output = []
+        assert(self.telnet)
+        if command is not None:
+            self.telnet.write('%s\n' % command)
+        while True:
+            line = self.telnet.read_until('\n')
+            output.append(line.rstrip())
+            if line.startswith('OK'):
+                return output
+            elif line.startswith('KO:'):
+                raise Exception('bad telnet response: %s' % line)
+
+    def _run_telnet(self, command):
+        if not self.telnet:
+            self.telnet = Telnet('localhost', self.port)
+            self._get_telnet_response()
+        return self._get_telnet_response(command)
+
+    def close(self):
+        if self.is_running and self._emulator_launched:
+            self.proc.terminate()
+            self.proc.wait()
+        if self._adb_started:
+            self._run_adb(['kill-server'])
+            self._adb_started = False
+        if self.proc:
+            retcode = self.proc.poll()
+            self.proc = None
+            if self._tmp_userdata:
+                os.remove(self._tmp_userdata)
+                self._tmp_userdata = None
+            return retcode
+        if self.logcat_proc:
+            self.logcat_proc.kill()
+        return 0
+
+    def _get_adb_devices(self):
+        offline = set()
+        online = set()
+        output = self._run_adb(['devices'])
+        for line in output.split('\n'):
+            m = self.deviceRe.match(line)
+            if m:
+                if m.group(3) == 'offline':
+                    offline.add(m.group(1))
+                else:
+                    online.add(m.group(1))
+        return (online, offline)
+
+    def restart(self):
+        if not self._emulator_launched:
+            return
+        self.close()
+        self.start()
+
+    def start_adb(self):
+        result = self._run_adb(['start-server'])
+        # We keep track of whether we've started adb or not, so we know
+        # if we need to kill it.
+        if 'daemon started successfully' in result:
+            self._adb_started = True
+        else:
+            self._adb_started = False
+
+    def connect(self):
+        self._check_for_adb()
+        self.start_adb()
+
+        online, offline = self._get_adb_devices()
+        now = datetime.datetime.now()
+        while online == set([]):
+            time.sleep(1)
+            if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
+                raise Exception('timed out waiting for emulator to be available')
+            online, offline = self._get_adb_devices()
+        self.port = int(list(online)[0])
+
+    @abstractmethod
+    def _locate_files(self):
+        pass
+
+    def start(self):
+        self._locate_files()
+        self.start_adb()
+
+        qemu_args = self.args[:]
+        if self.copy_userdata:
+            # Make a copy of the userdata.img for this instance of the emulator
+            # to use.
+            self._tmp_userdata = tempfile.mktemp(prefix='emulator')
+            shutil.copyfile(self.dataImg, self._tmp_userdata)
+            qemu_args[qemu_args.index('-data') + 1] = self._tmp_userdata
+
+        original_online, original_offline = self._get_adb_devices()
+
+        self.proc = subprocess.Popen(qemu_args,
+                                     stdout=subprocess.PIPE,
+                                     stderr=subprocess.PIPE)
+
+        online, offline = self._get_adb_devices()
+        now = datetime.datetime.now()
+        while online - original_online == set([]):
+            time.sleep(1)
+            if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
+                raise Exception('timed out waiting for emulator to start')
+            online, offline = self._get_adb_devices()
+        self.port = int(list(online - original_online)[0])
+        self._emulator_launched = True
+
+        if self.logcat_dir:
+            self.save_logcat()
+
+        # setup DNS fix for networking
+        self._run_adb(['-s', 'emulator-%d' % self.port,
+                       'shell', 'setprop', 'net.dns1', '10.0.2.3'])
+
+    def _save_logcat_proc(self, filename, cmd):
+        self.logcat_proc = LogcatProc(filename, cmd)
+        self.logcat_proc.run()
+        self.logcat_proc.waitForFinish()
+        self.logcat_proc = None
+
+    def rotate_log(self, srclog, index=1):
+        """ Rotate a logfile, by recursively rotating logs further in the sequence,
+            deleting the last file if necessary.
+        """
+        destlog = os.path.join(self.logcat_dir, 'emulator-%d.%d.log' % (self.port, index))
+        if os.path.exists(destlog):
+            if index == 3:
+                os.remove(destlog)
+            else:
+                self.rotate_log(destlog, index + 1)
+        shutil.move(srclog, destlog)
+
+    def save_logcat(self):
+        """ Save the output of logcat to a file.
+        """
+        filename = os.path.join(self.logcat_dir, "emulator-%d.log" % self.port)
+        if os.path.exists(filename):
+            self.rotate_log(filename)
+        cmd = [self.adb, '-s', 'emulator-%d' % self.port, 'logcat']
+
+        # We do this in a separate process because we call mozprocess's
+        # waitForFinish method to process logcat's output, and this method
+        # blocks.
+        proc = multiprocessing.Process(target=self._save_logcat_proc, args=(filename, cmd))
+        proc.daemon = True
+        proc.start()
+
+    def setup_port_forwarding(self, remote_port):
+        """ Set up TCP port forwarding to the specified port on the device,
+            using any availble local port, and return the local port.
+        """
+
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        s.bind(("", 0))
+        local_port = s.getsockname()[1]
+        s.close()
+
+        self._run_adb(['-s', 'emulator-%d' % self.port, 'forward',
+                       'tcp:%d' % local_port,
+                       'tcp:%d' % remote_port])
+
+        self.local_port = local_port
+
+        return local_port
+
+    def wait_for_port(self, timeout=300):
+        assert(self.local_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(('localhost', self.local_port))
+                data = sock.recv(16)
+                sock.close()
+                if '"from"' in data:
+                    return True
+            except:
+                import traceback
+                print traceback.format_exc()
+            time.sleep(1)
+        return False
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/emulator_battery.py
@@ -0,0 +1,52 @@
+# 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/.
+
+class EmulatorBattery(object):
+
+    def __init__(self, emulator):
+        self.emulator = emulator
+
+    def get_state(self):
+        status = {}
+        state = {}
+
+        response = self.emulator._run_telnet('power display')
+        for line in response:
+            if ':' in line:
+                field, value = line.split(':')
+                value = value.strip()
+                if value == 'true':
+                    value = True
+                elif value == 'false':
+                    value = False
+                elif field == 'capacity':
+                    value = float(value)
+                status[field] = value
+
+        state['level'] = status.get('capacity', 0.0) / 100
+        if status.get('AC') == 'online':
+            state['charging'] = True
+        else:
+            state['charging'] = False
+
+        return state
+
+    def get_charging(self):
+        return self.get_state()['charging']
+
+    def get_level(self):
+        return self.get_state()['level']
+
+    def set_level(self, level):
+        self.emulator._run_telnet('power capacity %d' % (level * 100))
+
+    def set_charging(self, charging):
+        if charging:
+            cmd = 'power ac on'
+        else:
+            cmd = 'power ac off'
+        self.emulator._run_telnet(cmd)
+
+    charging = property(get_charging, set_charging)
+    level = property(get_level, set_level)
deleted file mode 100644
--- a/testing/mozbase/mozdevice/mozdevice/sutini.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# 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 StringIO
-import os
-import sys
-import tempfile
-
-from mozdevice.droid import DroidSUT
-from mozdevice.devicemanager import DMError
-
-USAGE = '%s <host>'
-INI_PATH_JAVA = '/data/data/com.mozilla.SUTAgentAndroid/files/SUTAgent.ini'
-INI_PATH_NEGATUS = '/data/local/SUTAgent.ini'
-SCHEMA = {'Registration Server': (('IPAddr', ''),
-                                  ('PORT', '28001'),
-                                  ('HARDWARE', ''),
-                                  ('POOL', '')),
-          'Network Settings': (('SSID', ''),
-                               ('AUTH', ''),
-                               ('ENCR', ''),
-                               ('EAP', ''))}
-
-def get_cfg(d, ini_path):
-    cfg = ConfigParser.RawConfigParser()
-    try:
-        cfg.readfp(StringIO.StringIO(d.pullFile(ini_path)), 'SUTAgent.ini')
-    except DMError:
-        # assume this is due to a missing file...
-        pass
-    return cfg
-
-
-def put_cfg(d, cfg, ini_path):
-    print 'Writing modified SUTAgent.ini...'
-    t = tempfile.NamedTemporaryFile(delete=False)
-    cfg.write(t)
-    t.close()
-    try:
-        d.pushFile(t.name, ini_path)
-    except DMError, e:
-        print e
-    else:
-        print 'Done.'
-    finally:
-        os.unlink(t.name)
-
-
-def set_opt(cfg, s, o, dflt):
-    prompt = '  %s' % o
-    try:
-        curval = cfg.get(s, o)
-    except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
-        curval = ''
-    if curval:
-        dflt = curval
-    prompt += ': '
-    if dflt:
-        prompt += '[%s] ' % dflt
-    newval = raw_input(prompt)
-    if not newval:
-        newval = dflt
-    if newval == curval:
-        return False
-    cfg.set(s, o, newval)
-    return True
-
-
-def bool_query(prompt, dflt):
-    while True:
-        i = raw_input('%s [%s] ' % (prompt, 'y' if dflt else 'n')).lower()
-        if not i or i[0] in ('y', 'n'):
-            break
-        print 'Enter y or n.'
-    return (not i and dflt) or (i and i[0] == 'y')
-
-
-def edit_sect(cfg, sect, opts):
-    changed_vals = False
-    if bool_query('Edit section %s?' % sect, False):
-        if not cfg.has_section(sect):
-            cfg.add_section(sect)
-        print '%s settings:' % sect
-        for opt, dflt in opts:
-            changed_vals |= set_opt(cfg, sect, opt, dflt)
-        print
-    else:
-        if cfg.has_section(sect) and bool_query('Delete section %s?' % sect,
-                                                False):
-            cfg.remove_section(sect)
-            changed_vals = True
-    return changed_vals
-
-
-def main():
-    try:
-        host = sys.argv[1]
-    except IndexError:
-        print USAGE % sys.argv[0]
-        sys.exit(1)
-    try:
-        d = DroidSUT(host, retryLimit=1)
-    except DMError, e:
-        print e
-        sys.exit(1)
-    # check if using Negatus and change path accordingly
-    ini_path = INI_PATH_JAVA
-    if 'Negatus' in d.agentProductName:
-        ini_path = INI_PATH_NEGATUS
-    cfg = get_cfg(d, ini_path)
-    if not cfg.sections():
-        print 'Empty or missing ini file.'
-    changed_vals = False
-    for sect, opts in SCHEMA.iteritems():
-        changed_vals |= edit_sect(cfg, sect, opts)
-    if changed_vals:
-        put_cfg(d, cfg, ini_path)
-    else:
-        print 'No changes.'
-
-
-if __name__ == '__main__':
-    main()
--- a/testing/mozbase/mozdevice/setup.py
+++ b/testing/mozbase/mozdevice/setup.py
@@ -1,29 +1,30 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
-PACKAGE_VERSION = '0.21'
+PACKAGE_VERSION = '0.18'
+
+deps = ['mozprocess == 0.8']
 
 setup(name='mozdevice',
       version=PACKAGE_VERSION,
       description="Mozilla-authored device management",
       long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='',
       author='Mozilla Automation and Testing Team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
       license='MPL',
       packages=['mozdevice'],
       include_package_data=True,
       zip_safe=False,
-      install_requires=[],
+      install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       dm = mozdevice.dmcli:cli
-      sutini = mozdevice.sutini:main
       """,
       )
--- a/testing/mozbase/mozdevice/sut_tests/dmunit.py
+++ b/testing/mozbase/mozdevice/sut_tests/dmunit.py
@@ -4,17 +4,16 @@
 
 from mozdevice import devicemanager
 from mozdevice import devicemanagerSUT
 import types
 import unittest
 
 ip = ''
 port = 0
-heartbeat_port = 0
 
 
 class DeviceManagerTestCase(unittest.TestCase):
     """DeviceManager tests should subclass this.
     """
 
     """Set to False in your derived class if this test
     should not be run on the Python agent.
@@ -22,16 +21,17 @@ class DeviceManagerTestCase(unittest.Tes
     runs_on_test_device = True
 
     def _setUp(self):
         """ Override this if you want set-up code in your test."""
         return
 
     def setUp(self):
         self.dm = devicemanagerSUT.DeviceManagerSUT(host=ip, port=port)
+        self.dm.debug = 3
         self.dmerror = devicemanager.DMError
         self.nettools = devicemanager.NetworkTools
         self._setUp()
 
 
 class DeviceManagerTestLoader(unittest.TestLoader):
 
     def __init__(self, isTestDevice=False):
--- a/testing/mozbase/mozdevice/sut_tests/runtests.py
+++ b/testing/mozbase/mozdevice/sut_tests/runtests.py
@@ -7,23 +7,19 @@ import os
 import re
 import unittest
 import sys
 
 import dmunit
 import genfiles
 
 
-def main(ip, port, heartbeat_port, scripts, directory, isTestDevice, verbose):
+def main(ip, port, scripts, directory, isTestDevice):
     dmunit.ip = ip
     dmunit.port = port
-    dmunit.heartbeat_port = heartbeat_port
-    if verbose:
-        from mozdevice.devicemanagerSUT import DeviceManagerSUT
-        DeviceManagerSUT.debug = 4
 
     suite = unittest.TestSuite()
 
     genfiles.gen_test_files()
 
     if scripts:
         # Ensure the user didn't include the .py on the name of the test file
         # (and get rid of it if they did)
@@ -66,31 +62,24 @@ if  __name__ == "__main__":
                       "to what's provided in $TEST_DEVICE or 127.0.0.1",
                       default=(env_ip or default_ip))
 
     parser.add_option("--port", action="store", type="int", dest="port",
                       help="Port of SUTAgent on device, defaults to "
                       "what's provided in $TEST_DEVICE or 20701",
                       default=(env_port or default_port))
 
-    parser.add_option("--heartbeat", action="store", type="int",
-                      dest="heartbeat_port", help="Port for heartbeat/data "
-                      "channel, defaults to 20700", default=20700)
-
     parser.add_option("--script", action="append", type="string",
                       dest="scripts", help="Name of test script to run, "
                       "can be specified multiple times", default=[])
 
     parser.add_option("--directory", action="store", type="string", dest="dir",
                       help="Directory to look for tests in, defaults to "
                       "current directory", default=os.getcwd())
 
     parser.add_option("--testDevice", action="store_true", dest="isTestDevice",
                       help="Specifies that the device is a local test agent",
                       default=False)
 
-    parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
-                      help="Verbose DeviceManager output", default=False)
-
     (options, args) = parser.parse_args()
 
-    main(options.ip, options.port, options.heartbeat_port, options.scripts,
-         options.dir, options.isTestDevice, options.verbose)
+    main(options.ip, options.port, options.scripts,
+         options.dir, options.isTestDevice)
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_cat2.py
@@ -0,0 +1,27 @@
+# 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 hashlib
+import os
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+
+
+class Cat2TestCase(DeviceManagerTestCase):
+
+    def runTest(self):
+        """This tests copying a binary file to and from the device the binary.
+           File is > 64K.
+        """
+        testroot = posixpath.join(self.dm.getDeviceRoot(), 'infratest')
+        self.dm.removeDir(testroot)
+        self.dm.mkDir(testroot)
+        origFile = open(os.path.join('test-files', 'mybinary.zip'), 'rb').read()
+        self.dm.pushFile(
+                         os.path.join('test-files', 'mybinary.zip'),
+                         posixpath.join(testroot, 'mybinary.zip'))
+        resultFile = self.dm.catFile(posixpath.join(testroot, 'mybinary.zip'))
+        self.assertEqual(hashlib.md5(origFile).hexdigest(),
+                         hashlib.md5(resultFile).hexdigest())
--- a/testing/mozbase/mozdevice/sut_tests/test_datachannel.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_datachannel.py
@@ -1,52 +1,56 @@
 # 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 re
 import socket
 from time import strptime
+import re
 
-from dmunit import DeviceManagerTestCase, heartbeat_port
+from dmunit import DeviceManagerTestCase
+
 
 class DataChannelTestCase(DeviceManagerTestCase):
 
     runs_on_test_device = False
 
     def runTest(self):
-        """This tests the heartbeat and the data channel.
+        """ This tests the heartbeat and the data channel
         """
         ip = self.dm.host
+        port = 20700
 
         # Let's connect
         self._datasock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         # Assume 60 seconds between heartbeats
         self._datasock.settimeout(float(60 * 2))
-        self._datasock.connect((ip, heartbeat_port))
+        self._datasock.connect((ip, port))
         self._connected = True
 
         # Let's listen
         numbeats = 0
         capturedHeader = False
-        while numbeats < 3:
+        while(numbeats < 3):
             data = self._datasock.recv(1024)
             print data
             self.assertNotEqual(len(data), 0)
 
             # Check for the header
             if not capturedHeader:
                 m = re.match(r"(.*?) trace output", data)
                 self.assertNotEqual(m, None,
                     'trace output line does not match. The line: ' + str(data))
+                lastHeartbeatTime = strptime(m.group(1), "%Y%m%d-%H:%M:%S")
                 capturedHeader = True
 
             # Check for standard heartbeat messsage
             m = re.match(r"(.*?) Thump thump - (.*)", data)
             if m == None:
                 # This isn't an error, it usually means we've obtained some
                 # unexpected data from the device
                 continue
 
             # Ensure it matches our format
             mHeartbeatTime = m.group(1)
             mHeartbeatTime = strptime(mHeartbeatTime, "%Y%m%d-%H:%M:%S")
+            mDeviceID = m.group(2)
             numbeats = numbeats + 1
--- a/testing/mozbase/mozdevice/sut_tests/test_exec.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_exec.py
@@ -1,23 +1,23 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+from StringIO import StringIO
 import posixpath
-from StringIO import StringIO
 
 from dmunit import DeviceManagerTestCase
 
-class ExecTestCase(DeviceManagerTestCase):
+
+class ProcessListTestCase(DeviceManagerTestCase):
 
     def runTest(self):
-        """Simple exec test, does not use env vars."""
+        """ simple exec test, does not use env vars """
         out = StringIO()
         filename = posixpath.join(self.dm.getDeviceRoot(), 'test_exec_file')
-        # Make sure the file was not already there
+        # make sure the file was not already there
         self.dm.removeFile(filename)
-        self.dm.shell(['dd', 'if=/dev/zero', 'of=%s' % filename, 'bs=1024',
-                       'count=1'], out)
-        # Check that the file has been created
+        self.dm.shell(['touch', filename], out)
+        # check that the file has been created
         self.assertTrue(self.dm.fileExists(filename))
-        # Clean up
+        # clean up
         self.dm.removeFile(filename)
--- a/testing/mozbase/mozdevice/sut_tests/test_exec_env.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_exec_env.py
@@ -1,31 +1,32 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+from StringIO import StringIO
 import os
 import posixpath
-from StringIO import StringIO
 
 from dmunit import DeviceManagerTestCase
 
-class ExecEnvTestCase(DeviceManagerTestCase):
+
+class ProcessListTestCase(DeviceManagerTestCase):
 
     def runTest(self):
-        """Exec test with env vars."""
-        # Push the file
+        """ simple exec test, does not use env vars """
+        # push the file
         localfile = os.path.join('test-files', 'test_script.sh')
         remotefile = posixpath.join(self.dm.getDeviceRoot(), 'test_script.sh')
         self.dm.pushFile(localfile, remotefile)
 
-        # Run the cmd
+        # run the cmd
         out = StringIO()
         self.dm.shell(['sh', remotefile], out, env={'THE_ANSWER': 42})
 
-        # Rewind the output file
+        # rewind the output file
         out.seek(0)
-        # Make sure first line is 42
+        # make sure first line is 42
         line = out.readline()
         self.assertTrue(int(line) == 42)
 
-        # Clean up
+        # clean up
         self.dm.removeFile(remotefile)
--- a/testing/mozbase/mozdevice/sut_tests/test_getdir.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_getdir.py
@@ -2,19 +2,19 @@
 # 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 os
 import posixpath
 import shutil
 import tempfile
 
-from mozdevice.devicemanager import DMError
 from dmunit import DeviceManagerTestCase
 
+
 class GetDirectoryTestCase(DeviceManagerTestCase):
 
     def _setUp(self):
         self.localsrcdir = tempfile.mkdtemp()
         os.makedirs(os.path.join(self.localsrcdir, 'push1', 'sub.1', 'sub.2'))
         path = os.path.join(self.localsrcdir,
                             'push1', 'sub.1', 'sub.2', 'testfile')
         file(path, 'w').close()
@@ -33,18 +33,22 @@ class GetDirectoryTestCase(DeviceManager
         self.dm.removeDir(testroot)
         self.dm.mkDir(testroot)
         self.dm.pushDir(
             os.path.join(self.localsrcdir, 'push1'),
             posixpath.join(testroot, 'push1'))
         # pushDir doesn't copy over empty directories, but we want to make sure
         # that they are retrieved correctly.
         self.dm.mkDir(posixpath.join(testroot, 'push1', 'emptysub'))
-        self.dm.getDirectory(posixpath.join(testroot, 'push1'),
-                             os.path.join(self.localdestdir, 'push1'))
+        filelist = self.dm.getDirectory(
+            posixpath.join(testroot, 'push1'),
+            os.path.join(self.localdestdir, 'push1'))
+        filelist.sort()
+        self.assertEqual(filelist, self.expected_filelist)
         self.assertTrue(os.path.exists(
             os.path.join(self.localdestdir,
                          'push1', 'sub.1', 'sub.2', 'testfile')))
         self.assertTrue(os.path.exists(
             os.path.join(self.localdestdir, 'push1', 'emptysub')))
-        self.assertRaises(DMError, self.dm.getDirectory,
-                '/dummy', os.path.join(self.localdestdir, '/none'))
+        filelist = self.dm.getDirectory('/dummy',
+            os.path.join(self.localdestdir, '/none'))
+        self.assertEqual(filelist, None)
         self.assertFalse(os.path.exists(self.localdestdir + '/none'))
--- a/testing/mozbase/mozdevice/sut_tests/test_info.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_info.py
@@ -1,19 +1,25 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from dmunit import DeviceManagerTestCase
 
+
 class InfoTestCase(DeviceManagerTestCase):
 
     runs_on_test_device = False
 
     def runTest(self):
-        """This tests the "info" command.
+        """ This tests the "info" command
         """
-        cmds = ('os', 'id', 'systime', 'uptime', 'screen', 'memory', 'power')
+        cmds = ('os', 'id', 'systime', 'uptime', 'screen',
+                'memory', 'power')
         for c in cmds:
             data = self.dm.getInfo(c)
             print c + str(data)
 
+        print " ==== Now we call them all ===="
+        #data = self.dm.getInfo('all')
+        #print str(data)
+
         # No real good way to verify this.  If it doesn't throw, we're ok.
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_isdir.py
@@ -0,0 +1,31 @@
+# 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 os
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+
+
+class IsDirTestCase(DeviceManagerTestCase):
+
+    def runTest(self):
+        """This tests the isDir() function.
+        """
+        testroot = posixpath.join(self.dm.getDeviceRoot(), 'infratest')
+        self.dm.removeDir(testroot)
+        self.dm.mkDir(testroot)
+        self.assertTrue(self.dm.isDir(testroot))
+        testdir = posixpath.join(testroot, 'testdir')
+        self.assertFalse(self.dm.isDir(testdir))
+        self.dm.mkDir(testdir)
+        self.assertTrue(self.dm.isDir(testdir))
+        self.dm.pushFile(os.path.join('test-files', 'mytext.txt'),
+                         posixpath.join(testdir, 'mytext.txt'))
+        self.assertFalse(self.dm.isDir(posixpath.join(testdir, 'mytext.txt')))
+        self.dm.removeDir(testroot)
+        self.assertFalse(self.dm.isDir(testroot))
+        self.assertFalse(self.dm.isDir(testdir))
+        self.assertFalse(self.dm.isDir(posixpath.join(testdir, 'mytext.txt')))
+        self.assertFalse(self.dm.isDir(posixpath.join('/', 'noroot', 'nosub')))
--- a/testing/mozbase/mozdevice/sut_tests/test_prompt.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_prompt.py
@@ -2,16 +2,17 @@
 # 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 re
 import socket
 
 from dmunit import DeviceManagerTestCase
 
+
 class PromptTestCase(DeviceManagerTestCase):
 
     def tearDown(self):
         if self.sock:
             self.sock.close()
 
     def runTest(self):
         """This tests getting a prompt from the device.
@@ -21,9 +22,9 @@ class PromptTestCase(DeviceManagerTestCa
         port = self.dm.port
 
         promptre = re.compile('.*\$\>\x00')
         data = ""
         self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         self.sock.connect((ip, int(port)))
         data = self.sock.recv(1024)
         print data
-        self.assertTrue(promptre.match(data))
+        self.assert_(promptre.match(data))
--- a/testing/mozbase/mozdevice/sut_tests/test_ps.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_ps.py
@@ -1,27 +1,28 @@
 # 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 re
+
 from dmunit import DeviceManagerTestCase
 
+
 class ProcessListTestCase(DeviceManagerTestCase):
 
     def runTest(self):
-        """This tests getting a process list from the device.
+        """ This tests getting a process list from the device
         """
         proclist = self.dm.getProcessList()
 
         # This returns a process list of the form:
-        # [[<procid>, <procname>], [<procid>, <procname>], ...]
+        # [[<procid>,<procname>],[<procid>,<procname>]...]
         # on android the userID is affixed to the process array:
-        # [[<procid>, <procname>, <userid>], ...]
+        # [[<procid>, <procname>, <userid>]...]
+        procid = re.compile('^[a-f0-9]+')
+        procname = re.compile('.+')
 
         self.assertNotEqual(len(proclist), 0)
 
         for item in proclist:
-            self.assertIsInstance(item[0], int)
-            self.assertIsInstance(item[1], str)
-            self.assertGreater(len(item[1]), 0)
-            if len(item) > 2:
-                self.assertIsInstance(item[2], int)
-
+            self.assert_(procid.match(item[0]))
+            self.assert_(procname.match(item[1]))
--- a/testing/mozbase/mozdevice/sut_tests/test_pull.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_pull.py
@@ -2,32 +2,32 @@
 # 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 hashlib
 import os
 import posixpath
 
 from dmunit import DeviceManagerTestCase
-from mozdevice.devicemanager import DMError
+
 
 class PullTestCase(DeviceManagerTestCase):
 
     def runTest(self):
         """Tests the "pull" command with a binary file.
         """
-        orig = hashlib.md5()
-        new = hashlib.md5()
+        m_orig = hashlib.md5()
+        m_new = hashlib.md5()
         local_test_file = os.path.join('test-files', 'mybinary.zip')
-        orig.update(file(local_test_file, 'r').read())
+        m_orig.update(file(local_test_file, 'r').read())
 
         testroot = self.dm.getDeviceRoot()
         remote_test_file = posixpath.join(testroot, 'mybinary.zip')
         self.dm.removeFile(remote_test_file)
         self.dm.pushFile(local_test_file, remote_test_file)
-        new.update(self.dm.pullFile(remote_test_file))
-        # Use hexdigest() instead of digest() since values are printed
+        m_new.update(self.dm.pullFile(remote_test_file))
+        # use hexdigest() instead of digest() since values are printed
         # if assert fails
-        self.assertEqual(orig.hexdigest(), new.hexdigest())
+        self.assertEqual(m_orig.hexdigest(), m_new.hexdigest())
 
         remote_missing_file = posixpath.join(testroot, 'doesnotexist')
-        self.dm.removeFile(remote_missing_file)  # Just to be sure
-        self.assertRaises(DMError, self.dm.pullFile, remote_missing_file)
+        self.dm.removeFile(remote_missing_file)  # just to be sure
+        self.assertEqual(self.dm.pullFile(remote_missing_file), None)
--- a/testing/mozbase/mozdevice/sut_tests/test_push1.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_push1.py
@@ -2,20 +2,21 @@
 # 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 os
 import posixpath
 
 from dmunit import DeviceManagerTestCase
 
+
 class Push1TestCase(DeviceManagerTestCase):
 
     def runTest(self):
-        """This tests copying a directory structure to the device.
+        """ This tests copying a directory structure to the device
         """
         dvroot = self.dm.getDeviceRoot()
         dvpath = posixpath.join(dvroot, 'infratest')
         self.dm.removeDir(dvpath)
         self.dm.mkDir(dvpath)
 
         p1 = os.path.join('test-files', 'push1')
         # Set up local stuff
@@ -24,14 +25,16 @@ class Push1TestCase(DeviceManagerTestCas
         except:
             pass
 
         if not os.path.exists(p1):
             os.makedirs(os.path.join(p1, 'sub.1', 'sub.2'))
         if not os.path.exists(os.path.join(p1, 'sub.1', 'sub.2', 'testfile')):
             file(os.path.join(p1, 'sub.1', 'sub.2', 'testfile'), 'w').close()
 
+        # push the directory
         self.dm.pushDir(p1, posixpath.join(dvpath, 'push1'))
 
-        self.assertTrue(
+        # verify
+        self.assert_(
             self.dm.dirExists(posixpath.join(dvpath, 'push1', 'sub.1')))
-        self.assertTrue(self.dm.dirExists(
+        self.assert_(self.dm.dirExists(
             posixpath.join(dvpath, 'push1', 'sub.1', 'sub.2')))
--- a/testing/mozbase/mozdevice/sut_tests/test_push2.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_push2.py
@@ -2,37 +2,38 @@
 # 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 os
 import posixpath
 
 from dmunit import DeviceManagerTestCase
 
+
 class Push2TestCase(DeviceManagerTestCase):
 
     def runTest(self):
-        """This tests copying a directory structure with files to the device.
+        """ This tests copying a directory structure with files to the device
         """
         testroot = posixpath.join(self.dm.getDeviceRoot(), 'infratest')
         self.dm.removeDir(testroot)
         self.dm.mkDir(testroot)
         path = posixpath.join(testroot, 'push2')
         self.dm.pushDir(os.path.join('test-files', 'push2'), path)
 
         # Let's walk the tree and make sure everything is there
         # though it's kind of cheesy, we'll use the validate file to compare
         # hashes - we use the client side hashing when testing the cat command
         # specifically, so that makes this a little less cheesy, I guess.
-        self.assertTrue(
+        self.assert_(
             self.dm.dirExists(posixpath.join(testroot, 'push2', 'sub1')))
-        self.assertTrue(self.dm.validateFile(
+        self.assert_(self.dm.validateFile(
             posixpath.join(testroot, 'push2', 'sub1', 'file1.txt'),
             os.path.join('test-files', 'push2', 'sub1', 'file1.txt')))
-        self.assertTrue(self.dm.validateFile(
+        self.assert_(self.dm.validateFile(
             posixpath.join(testroot, 'push2', 'sub1', 'sub1.1', 'file2.txt'),
             os.path.join('test-files', 'push2', 'sub1', 'sub1.1', 'file2.txt')))
-        self.assertTrue(self.dm.validateFile(
+        self.assert_(self.dm.validateFile(
             posixpath.join(testroot, 'push2', 'sub2', 'file3.txt'),
             os.path.join('test-files', 'push2', 'sub2', 'file3.txt')))
-        self.assertTrue(self.dm.validateFile(
+        self.assert_(self.dm.validateFile(
             posixpath.join(testroot, 'push2', 'file4.bin'),
             os.path.join('test-files', 'push2', 'file4.bin')))
--- a/testing/mozbase/mozdevice/sut_tests/test_pushbinary.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_pushbinary.py
@@ -2,17 +2,19 @@
 # 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 os
 import posixpath
 
 from dmunit import DeviceManagerTestCase
 
+
 class PushBinaryTestCase(DeviceManagerTestCase):
 
     def runTest(self):
         """This tests copying a binary file.
         """
         testroot = self.dm.getDeviceRoot()
         self.dm.removeFile(posixpath.join(testroot, 'mybinary.zip'))
-        self.dm.pushFile(os.path.join('test-files', 'mybinary.zip'),
-                         posixpath.join(testroot, 'mybinary.zip'))
+        self.assert_(self.dm.pushFile(
+            os.path.join('test-files', 'mybinary.zip'),
+            posixpath.join(testroot, 'mybinary.zip')))
--- a/testing/mozbase/mozdevice/sut_tests/test_pushsmalltext.py
+++ b/testing/mozbase/mozdevice/sut_tests/test_pushsmalltext.py
@@ -2,17 +2,19 @@
 # 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 os
 import posixpath
 
 from dmunit import DeviceManagerTestCase
 
+
 class PushSmallTextTestCase(DeviceManagerTestCase):
 
     def runTest(self):
         """This tests copying a small text file.
         """
         testroot = self.dm.getDeviceRoot()
         self.dm.removeFile(posixpath.join(testroot, 'smalltext.txt'))
-        self.dm.pushFile(os.path.join('test-files', 'smalltext.txt'),
-                         posixpath.join(testroot, 'smalltext.txt'))
+        self.assert_(self.dm.pushFile(
+            os.path.join('test-files', 'smalltext.txt'),
+            posixpath.join(testroot, 'smalltext.txt')))
--- a/testing/mozbase/mozdevice/tests/sut_mkdir.py
+++ b/testing/mozbase/mozdevice/tests/sut_mkdir.py
@@ -1,63 +1,41 @@
-# Any copyright is dedicated to the Public Domain.
-# http://creativecommons.org/publicdomain/zero/1.0/
-
+from sut import MockAgent
 import mozdevice
 import unittest
-from sut import MockAgent
 
-class MkDirsTest(unittest.TestCase):
+class PushTest(unittest.TestCase):
 
     def test_mkdirs(self):
-        subTests = [{'cmds': [('isdir /mnt/sdcard/baz/boop', 'FALSE'),
-                              ('isdir /mnt', 'TRUE'),
-                              ('isdir /mnt/sdcard', 'TRUE'),
-                              ('isdir /mnt/sdcard/baz', 'FALSE'),
-                              ('mkdr /mnt/sdcard/baz',
-                               '/mnt/sdcard/baz successfully created'),
-                              ('isdir /mnt/sdcard/baz/boop', 'FALSE'),
-                              ('mkdr /mnt/sdcard/baz/boop',
-                               '/mnt/sdcard/baz/boop successfully created')],
-                     'expectException': False},
-                    {'cmds': [('isdir /mnt/sdcard/baz/boop', 'FALSE'),
-                              ('isdir /mnt', 'TRUE'),
-                              ('isdir /mnt/sdcard', 'TRUE'),
-                              ('isdir /mnt/sdcard/baz', 'FALSE'),
-                              ('mkdr /mnt/sdcard/baz',
-                               '##AGENT-WARNING## Could not create the directory /mnt/sdcard/baz')],
-                     'expectException': True},
+        subTests = [ { 'cmds': [ ("isdir /mnt/sdcard/baz/boop", "FALSE"),
+                                 ("isdir /mnt", "TRUE"),
+                                 ("isdir /mnt/sdcard", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "FALSE"),
+                                 ("mkdr /mnt/sdcard/baz",
+                                  "/mnt/sdcard/baz successfully created"),
+                                 ("isdir /mnt/sdcard/baz/boop", "FALSE"),
+                                 ("mkdr /mnt/sdcard/baz/boop",
+                                  "/mnt/sdcard/baz/boop successfully created") ],
+                       'expectException': False },
+                     { 'cmds': [ ("isdir /mnt/sdcard/baz/boop", "FALSE"),
+                                 ("isdir /mnt", "TRUE"),
+                                 ("isdir /mnt/sdcard", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "FALSE"),
+                                 ("mkdr /mnt/sdcard/baz",
+                                  "##AGENT-WARNING## Could not create the directory /mnt/sdcard/baz") ],
+                       'expectException': True },
                      ]
         for subTest in subTests:
-            a = MockAgent(self, commands=subTest['cmds'])
+            a = MockAgent(self, commands = subTest['cmds'])
 
             exceptionThrown = False
             try:
                 mozdevice.DroidSUT.debug = 4
-                d = mozdevice.DroidSUT('127.0.0.1', port=a.port)
-                d.mkDirs('/mnt/sdcard/baz/boop/bip')
-            except mozdevice.DMError:
+                d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+                d.mkDirs("/mnt/sdcard/baz/boop/bip")
+            except mozdevice.DMError, e:
                 exceptionThrown = True
             self.assertEqual(exceptionThrown, subTest['expectException'])
 
             a.wait()
 
-    def test_repeated_path_part(self):
-        """
-        Ensure that all dirs are created when last path part also found
-        earlier in the path (bug 826492).
-        """
-
-        cmds = [('isdir /mnt/sdcard/foo', 'FALSE'),
-                ('isdir /mnt', 'TRUE'),
-                ('isdir /mnt/sdcard', 'TRUE'),
-                ('isdir /mnt/sdcard/foo', 'FALSE'),
-                ('mkdr /mnt/sdcard/foo',
-                 '/mnt/sdcard/foo successfully created')]
-        a = MockAgent(self, commands=cmds)
-        mozdevice.DroidSUT.debug = 4
-        d = mozdevice.DroidSUT('127.0.0.1', port=a.port)
-        d.mkDirs('/mnt/sdcard/foo/foo')
-        a.wait()
-
-
 if __name__ == '__main__':
     unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozfile/README.md
@@ -0,0 +1,4 @@
+mozfile is a convenience library for taking care of some common file-related
+tasks in automated testing, such as extracting files or recursively removing
+directories.
+
--- a/testing/mozbase/mozfile/mozfile/mozfile.py
+++ b/testing/mozbase/mozfile/mozfile/mozfile.py
@@ -1,19 +1,18 @@
 # 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 os
 import tarfile
 import tempfile
-import urlparse
 import zipfile
 
-__all__ = ['extract_tarball', 'extract_zip', 'extract', 'is_url', 'rmtree', 'NamedTemporaryFile']
+__all__ = ['extract_tarball', 'extract_zip', 'extract', 'rmtree', 'NamedTemporaryFile']
 
 
 ### utilities for extracting archives
 
 def extract_tarball(src, dest):
     """extract a .tar file"""
 
     bundle = tarfile.open(src)
@@ -23,25 +22,17 @@ def extract_tarball(src, dest):
         bundle.extract(name, path=dest)
     bundle.close()
     return namelist
 
 
 def extract_zip(src, dest):
     """extract a zip file"""
 
-    if isinstance(src, zipfile.ZipFile):
-        bundle = src
-    else:
-        try:
-            bundle = zipfile.ZipFile(src)
-        except Exception, e:
-            print "src: %s" % src
-            raise
-
+    bundle = zipfile.ZipFile(src)
     namelist = bundle.namelist()
 
     for name in namelist:
         filename = os.path.realpath(os.path.join(dest, name))
         if name.endswith('/'):
             os.makedirs(filename)
         else:
             path = os.path.dirname(filename)
@@ -182,20 +173,8 @@ class NamedTemporaryFile(object):
         self._unlinked = True
 
     def __del__(self):
         if self.__dict__['_unlinked']:
             return
 
         self.file.__exit__(None, None, None)
         os.unlink(self.__dict__['_path'])
-
-
-def is_url(thing):
-    """
-    Return True if thing looks like a URL.
-    """
-
-    parsed = urlparse.urlparse(thing)
-    if 'scheme' in parsed:
-        return len(parsed.scheme) >= 2
-    else:
-        return len(parsed[0]) >= 2
--- a/testing/mozbase/mozfile/setup.py
+++ b/testing/mozbase/mozfile/setup.py
@@ -1,15 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
-PACKAGE_VERSION = '0.3'
+PACKAGE_VERSION = '0.2'
 
 setup(name='mozfile',
       version=PACKAGE_VERSION,
       description="Library of file utilities for use in Mozilla testing",
       long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
deleted file mode 100644
--- a/testing/mozbase/mozfile/tests/is_url.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env python
-
-"""
-tests for is_url
-"""
-
-import unittest
-from mozfile import is_url
-
-class TestIsUrl(unittest.TestCase):
-    """test the is_url function"""
-
-    def test_is_url(self):
-        self.assertTrue(is_url('http://mozilla.org'))
-        self.assertFalse(is_url('/usr/bin/mozilla.org'))
-        self.assertTrue(is_url('file:///usr/bin/mozilla.org'))
-        self.assertFalse(is_url('c:\foo\bar'))
-
-if __name__ == '__main__':
-    unittest.main()
--- a/testing/mozbase/mozfile/tests/manifest.ini
+++ b/testing/mozbase/mozfile/tests/manifest.ini
@@ -1,2 +1,1 @@
 [test.py]
-[is_url.py]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/README.md
@@ -0,0 +1,168 @@
+[mozprocess](https://github.com/mozilla/mozbase/tree/master/mozprocess)
+provides python process management via an operating system
+and platform transparent interface to Mozilla platforms of interest.
+Mozprocess aims to provide the ability
+to robustly terminate a process (by timeout or otherwise), along with
+any child processes, on Windows, OS X, and Linux. Mozprocess utilizes
+and extends `subprocess.Popen` to these ends.
+
+
+# API
+
+[mozprocess.processhandler:ProcessHandler](https://github.com/mozilla/mozbase/blob/master/mozprocess/mozprocess/processhandler.py)
+is the central exposed API for mozprocess.  `ProcessHandler` utilizes
+a contained subclass of [subprocess.Popen](http://docs.python.org/library/subprocess.html),
+`Process`, which does the brunt of the process management.
+
+## Basic usage
+
+    process = ProcessHandler(['command', '-line', 'arguments'],
+                             cwd=None, # working directory for cmd; defaults to None
+                             env={},   # environment to use for the process; defaults to os.environ
+                             )
+    process.run(timeout=60) # seconds
+    process.wait()
+
+`ProcessHandler` offers several other properties and methods as part of its API:
+
+    def __init__(self,
+                 cmd,
+                 args=None,
+                 cwd=None,
+                 env=None,
+                 ignore_children = False,
+                 processOutputLine=(),
+                 onTimeout=(),
+                 onFinish=(),
+                 **kwargs):
+        """
+        cmd = Command to run
+        args = array of arguments (defaults to None)
+        cwd = working directory for cmd (defaults to None)
+        env = environment to use for the process (defaults to os.environ)
+        ignore_children = when True, causes system to ignore child processes,
+        defaults to False (which tracks child processes)
+        processOutputLine = handlers to process the output line
+        onTimeout = handlers for timeout event
+        kwargs = keyword args to pass directly into Popen
+
+        NOTE: Child processes will be tracked by default. If for any reason
+        we are unable to track child processes and ignore_children is set to False,
+        then we will fall back to only tracking the root process. The fallback
+        will be logged.
+        """
+
+    @property
+    def timedOut(self):
+        """True if the process has timed out."""
+
+
+    def run(self, timeout=None, outputTimeout=None):
+        """
+        Starts the process.
+
+        If timeout is not None, the process will be allowed to continue for
+        that number of seconds before being killed.
+
+        If outputTimeout is not None, the process will be allowed to continue
+        for that number of seconds without producing any output before
+        being killed.
+        """
+
+    def kill(self):
+        """
+        Kills the managed process and if you created the process with
+        'ignore_children=False' (the default) then it will also
+        also kill all child processes spawned by it.
+        If you specified 'ignore_children=True' when creating the process,
+        only the root process will be killed.
+
+        Note that this does not manage any state, save any output etc,
+        it immediately kills the process.
+        """
+
+    def readWithTimeout(self, f, timeout):
+        """
+        Try to read a line of output from the file object |f|.
+        |f| must be a pipe, like the |stdout| member of a subprocess.Popen
+        object created with stdout=PIPE. If no output
+        is received within |timeout| seconds, return a blank line.
+        Returns a tuple (line, did_timeout), where |did_timeout| is True
+        if the read timed out, and False otherwise.
+
+        Calls a private member because this is a different function based on
+        the OS
+        """
+
+    def processOutputLine(self, line):
+        """Called for each line of output that a process sends to stdout/stderr."""
+        for handler in self.processOutputLineHandlers:
+            handler(line)
+
+    def onTimeout(self):
+        """Called when a process times out."""
+        for handler in self.onTimeoutHandlers:
+            handler()
+
+    def onFinish(self):
+        """Called when a process finishes without a timeout."""
+        for handler in self.onFinishHandlers:
+            handler()
+
+    def wait(self, timeout=None):
+        """
+        Waits until all output has been read and the process is 
+        terminated.
+
+        If timeout is not None, will return after timeout seconds.
+        This timeout only causes the wait function to return and
+        does not kill the process.
+        """
+
+See https://github.com/mozilla/mozbase/blob/master/mozprocess/mozprocess/processhandler.py
+for the python implementation.
+
+`ProcessHandler` extends `ProcessHandlerMixin` which by default prints the
+output, logs to a file (if specified), and stores the output (if specified, by
+default `True`).  `ProcessHandlerMixin`, by default, does none of these things
+and has no handlers for `onTimeout`, `processOutput`, or `onFinish`.
+
+`ProcessHandler` may be subclassed to handle process timeouts (by overriding
+the `onTimeout()` method), process completion (by overriding
+`onFinish()`), and to process the command output (by overriding
+`processOutputLine()`).
+
+## Examples
+
+In the most common case, a process_handler is created, then run followed by wait are called:
+
+    proc_handler = ProcessHandler([cmd, args])
+    proc_handler.run(outputTimeout=60) # will time out after 60 seconds without output
+    proc_handler.wait()
+
+Often, the main thread will do other things:
+
+    proc_handler = ProcessHandler([cmd, args])
+    proc_handler.run(timeout=60) # will time out after 60 seconds regardless of output
+    do_other_work()
+
+    if proc_handler.proc.poll() is None:
+        proc_handler.wait()
+
+By default output is printed to stdout, but anything is possible:
+
+    # this example writes output to both stderr and a file called 'output.log'
+    def some_func(line):
+        print >> sys.stderr, line
+
+        with open('output.log', 'a') as log:
+            log.write('%s\n' % line)
+
+    proc_handler = ProcessHandler([cmd, args], processOutputLine=some_func)
+    proc_handler.run()
+    proc_handler.wait()
+
+# TODO
+
+- Document improvements over `subprocess.Popen.kill`
+- Introduce test the show improvements over `subprocess.Popen.kill`
--- a/testing/mozbase/mozprocess/setup.py
+++ b/testing/mozbase/mozprocess/setup.py
@@ -1,20 +1,28 @@
 # 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 os
 from setuptools import setup
 
-PACKAGE_VERSION = '0.9'
+PACKAGE_VERSION = '0.8'
+
+# take description from README
+here = os.path.dirname(os.path.abspath(__file__))
+try:
+    description = file(os.path.join(here, 'README.md')).read()
+except (OSError, IOError):
+    description = ''
 
 setup(name='mozprocess',
       version=PACKAGE_VERSION,
       description="Mozilla-authored process handling",
-      long_description='see http://mozbase.readthedocs.org/',
+      long_description=description,
       classifiers=['Environment :: Console',
                    'Intended Audience :: Developers',
                    'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
                    'Natural Language :: English',
                    'Operating System :: OS Independent',
                    'Programming Language :: Python',
                    'Topic :: Software Development :: Libraries :: Python Modules',
                    ],
--- a/testing/mozbase/mozprocess/tests/manifest.ini
+++ b/testing/mozbase/mozprocess/tests/manifest.ini
@@ -1,1 +1,2 @@
-[test_mozprocess.py]
+[mozprocess1.py]
+[mozprocess2.py]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/tests/mozprocess1.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+
+# 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 os
+import subprocess
+import sys
+import unittest
+from time import sleep
+
+from mozprocess import processhandler
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+def make_proclaunch(aDir):
+    """
+        Makes the proclaunch executable.
+        Params:
+            aDir - the directory in which to issue the make commands
+        Returns:
+            the path to the proclaunch executable that is generated
+    """
+    # Ideally make should take care of this, but since it doesn't - on windows,
+    # anyway, let's just call out both targets explicitly.
+    p = subprocess.call(["make", "-C", "iniparser"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=aDir)
+    p = subprocess.call(["make"],stdout=subprocess.PIPE, stderr=subprocess.PIPE ,cwd=aDir)
+    if sys.platform == "win32":
+        exepath = os.path.join(aDir, "proclaunch.exe")
+    else:
+        exepath = os.path.join(aDir, "proclaunch")
+    return exepath
+
+def check_for_process(processName):
+    """
+        Use to determine if process of the given name is still running.
+
+        Returns:
+        detected -- True if process is detected to exist, False otherwise
+        output -- if process exists, stdout of the process, '' otherwise
+    """
+    output = ''
+    if sys.platform == "win32":
+        # On windows we use tasklist
+        p1 = subprocess.Popen(["tasklist"], stdout=subprocess.PIPE)
+        output = p1.communicate()[0]
+        detected = False
+        for line in output.splitlines():
+            if processName in line:
+                detected = True
+                break
+    else:
+        p1 = subprocess.Popen(["ps", "-ef"], stdout=subprocess.PIPE)
+        p2 = subprocess.Popen(["grep", processName], stdin=p1.stdout, stdout=subprocess.PIPE)
+        p1.stdout.close()
+        output = p2.communicate()[0]
+        detected = False
+        for line in output.splitlines():
+            if "grep %s" % processName in line:
+                continue
+            elif processName in line and not 'defunct' in line: 
+                detected = True
+                break
+
+    return detected, output
+
+
+class ProcTest1(unittest.TestCase):
+
+    def __init__(self, *args, **kwargs):
+
+        # Ideally, I'd use setUpClass but that only exists in 2.7.
+        # So, we'll do this make step now.
+        self.proclaunch = make_proclaunch(here)
+        unittest.TestCase.__init__(self, *args, **kwargs)
+
+    def test_process_normal_finish(self):
+        """Process is started, runs to completion while we wait for it"""
+
+        p = processhandler.ProcessHandler([self.proclaunch, "process_normal_finish.ini"],
+                                          cwd=here)
+        p.run()
+        p.wait()
+
+        detected, output = check_for_process(self.proclaunch)
+        self.determine_status(detected,
+                              output,
+                              p.proc.returncode,
+                              p.didTimeout)
+
+    def test_process_waittimeout(self):
+        """ Process is started, runs but we time out waiting on it
+            to complete
+        """
+        p = processhandler.ProcessHandler([self.proclaunch, "process_waittimeout.ini"],
+                                          cwd=here)
+        p.run(timeout=10)
+        p.wait()
+
+        detected, output = check_for_process(self.proclaunch)
+        self.determine_status(detected,
+                              output,
+                              p.proc.returncode,
+                              p.didTimeout,
+                              False,
+                              ['returncode', 'didtimeout'])
+
+    def test_process_kill(self):
+        """ Process is started, we kill it
+        """
+        p = processhandler.ProcessHandler([self.proclaunch, "process_normal_finish.ini"],
+                                          cwd=here)
+        p.run()
+        p.kill()
+
+        detected, output = check_for_process(self.proclaunch)
+        self.determine_status(detected,
+                              output,
+                              p.proc.returncode,
+                              p.didTimeout)
+
+    def determine_status(self,
+                         detected=False,
+                         output='',
+                         returncode=0,
+                         didtimeout=False,
+                         isalive=False,
+                         expectedfail=[]):
+        """
+        Use to determine if the situation has failed.
+        Parameters:
+            detected -- value from check_for_process to determine if the process is detected
+            output -- string of data from detected process, can be ''
+            returncode -- return code from process, defaults to 0
+            didtimeout -- True if process timed out, defaults to False
+            isalive -- Use True to indicate we pass if the process exists; however, by default
+                       the test will pass if the process does not exist (isalive == False)
+            expectedfail -- Defaults to [], used to indicate a list of fields that are expected to fail
+        """
+        if 'returncode' in expectedfail:
+            self.assertTrue(returncode, "Detected an unexpected return code of: %s" % returncode)
+        elif not isalive:
+            self.assertTrue(returncode == 0, "Detected non-zero return code of: %d" % returncode)
+
+        if 'didtimeout' in expectedfail:
+            self.assertTrue(didtimeout, "Detected that process didn't time out")
+        else:
+            self.assertTrue(not didtimeout, "Detected that process timed out")
+
+        if isalive:
+            self.assertTrue(detected, "Detected process is not running, process output: %s" % output)
+        else:
+            self.assertTrue(not detected, "Detected process is still running, process output: %s" % output)
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/tests/mozprocess2.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python
+
+# 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 os
+import subprocess
+import sys
+import unittest
+from time import sleep
+
+from mozprocess import processhandler
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+# This tests specifically the case reported in bug 671316
+# TODO: Because of the way mutt works we can't just load a utils.py in here.
+#       so, for all process handler tests, copy these two
+#       utility functions to to the top of your source.
+
+def make_proclaunch(aDir):
+    """
+        Makes the proclaunch executable.
+        Params:
+            aDir - the directory in which to issue the make commands
+        Returns:
+            the path to the proclaunch executable that is generated
+    """
+    # Ideally make should take care of this, but since it doesn't - on windows,
+    # anyway, let's just call out both targets explicitly.
+    p = subprocess.call(["make", "-C", "iniparser"],stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=aDir)
+    p = subprocess.call(["make"],stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=aDir)
+    if sys.platform == "win32":
+        exepath = os.path.join(aDir, "proclaunch.exe")
+    else:
+        exepath = os.path.join(aDir, "proclaunch")
+    return exepath
+
+def check_for_process(processName):
+    """
+        Use to determine if process is still running.
+
+        Returns:
+        detected -- True if process is detected to exist, False otherwise
+        output -- if process exists, stdout of the process, '' otherwise
+    """
+    output = ''
+    if sys.platform == "win32":
+        # On windows we use tasklist
+        p1 = subprocess.Popen(["tasklist"], stdout=subprocess.PIPE)
+        output = p1.communicate()[0]
+        detected = False
+        for line in output.splitlines():
+            if processName in line:
+                detected = True
+                break
+    else:
+        p1 = subprocess.Popen(["ps", "-ef"], stdout=subprocess.PIPE)
+        p2 = subprocess.Popen(["grep", processName], stdin=p1.stdout, stdout=subprocess.PIPE)
+        p1.stdout.close()
+        output = p2.communicate()[0]
+        detected = False
+        for line in output.splitlines():
+            if "grep %s" % processName in line:
+                continue
+            elif processName in line and not 'defunct' in line:
+                detected = True
+                break
+
+    return detected, output
+
+class ProcTest2(unittest.TestCase):
+
+    def __init__(self, *args, **kwargs):
+
+        # Ideally, I'd use setUpClass but that only exists in 2.7.
+        # So, we'll do this make step now.
+        self.proclaunch = make_proclaunch(here)
+        unittest.TestCase.__init__(self, *args, **kwargs)
+
+    def test_process_waitnotimeout(self):
+        """ Process is started, runs to completion before our wait times out
+        """
+        p = processhandler.ProcessHandler([self.proclaunch,
+                                          "process_waittimeout_10s.ini"],
+                                          cwd=here)
+        p.run(timeout=30)
+        p.wait()
+
+        detected, output = check_for_process(self.proclaunch)
+        self.determine_status(detected,
+                              output,
+                              p.proc.returncode,
+                              p.didTimeout)
+
+    def test_process_wait(self):
+        """ Process is started runs to completion while we wait indefinitely
+        """
+
+        p = processhandler.ProcessHandler([self.proclaunch,
+                                          "process_waittimeout_10s.ini"],
+                                          cwd=here)
+        p.run()
+        p.wait()
+
+        detected, output = check_for_process(self.proclaunch)
+        self.determine_status(detected,
+                              output,
+                              p.proc.returncode,
+                              p.didTimeout)
+
+    def test_process_waittimeout(self):
+        """
+        Process is started, then wait is called and times out.
+        Process is still running and didn't timeout
+        """
+        p = processhandler.ProcessHandler([self.proclaunch,
+                                          "process_waittimeout_10s.ini"],
+                                          cwd=here)
+
+        p.run()
+        p.wait(timeout=5)
+
+        detected, output = check_for_process(self.proclaunch)
+        self.determine_status(detected,
+                              output,
+                              p.proc.returncode,
+                              p.didTimeout,
+                              True,
+                              [])
+
+    def test_process_output_twice(self):
+        """
+        Process is started, then processOutput is called a second time explicitly
+        """
+        p = processhandler.ProcessHandler([self.proclaunch,
+                                          "process_waittimeout_10s.ini"],
+                                          cwd=here)
+
+        p.run()
+        p.processOutput(timeout=5)
+        p.wait()
+
+        detected, output = check_for_process(self.proclaunch)
+        self.determine_status(detected,
+                              output,
+                              p.proc.returncode,
+                              p.didTimeout,
+                              False,
+                              [])
+
+
+    def determine_status(self,
+                         detected=False,
+                         output = '',
+                         returncode = 0,
+                         didtimeout = False,
+                         isalive=False,
+                         expectedfail=[]):
+        """
+        Use to determine if the situation has failed.
+        Parameters:
+            detected -- value from check_for_process to determine if the process is detected
+            output -- string of data from detected process, can be ''
+            returncode -- return code from process, defaults to 0
+            didtimeout -- True if process timed out, defaults to False
+            isalive -- Use True to indicate we pass if the process exists; however, by default
+                       the test will pass if the process does not exist (isalive == False)
+            expectedfail -- Defaults to [], used to indicate a list of fields that are expected to fail
+        """
+        if 'returncode' in expectedfail:
+            self.assertTrue(returncode, "Detected an unexpected return code of: %s" % returncode)
+        elif not isalive:
+            self.assertTrue(returncode == 0, "Detected non-zero return code of: %d" % returncode)
+
+        if 'didtimeout' in expectedfail:
+            self.assertTrue(didtimeout, "Detected that process didn't time out")
+        else:
+            self.assertTrue(not didtimeout, "Detected that process timed out")
+
+        if isalive:
+            self.assertTrue(detected, "Detected process is not running, process output: %s" % output)
+        else:
+            self.assertTrue(not detected, "Detected process is still running, process output: %s" % output)
+
+if __name__ == '__main__':
+    unittest.main()
deleted file mode 100644
--- a/testing/mozbase/mozprocess/tests/test_mozprocess.py
+++ /dev/null
@@ -1,250 +0,0 @@
-#!/usr/bin/env python
-
-# 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 os
-import subprocess
-import sys
-import unittest
-from time import sleep
-
-from mozprocess import processhandler
-
-here = os.path.dirname(os.path.abspath(__file__))
-
-def make_proclaunch(aDir):
-    """
-        Makes the proclaunch executable.
-        Params:
-            aDir - the directory in which to issue the make commands
-        Returns:
-            the path to the proclaunch executable that is generated
-    """
-    # Ideally make should take care of this, but since it doesn't,
-    # on windows anyway, let's just call out both targets explicitly.
-    p = subprocess.call(["make", "-C", "iniparser"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=aDir)
-    p = subprocess.call(["make"],stdout=subprocess.PIPE, stderr=subprocess.PIPE ,cwd=aDir)
-    if sys.platform == "win32":
-        exepath = os.path.join(aDir, "proclaunch.exe")
-    else:
-        exepath = os.path.join(aDir, "proclaunch")
-    return exepath
-
-def check_for_process(processName):
-    """
-        Use to determine if process of the given name is still running.
-
-        Returns:
-        detected -- True if process is detected to exist, False otherwise
-        output -- if process exists, stdout of the process, '' otherwise
-    """
-    # TODO: replace with
-    # https://github.com/mozilla/mozbase/blob/master/mozprocess/mozprocess/pid.py
-    # which should be augmented from talos
-    # see https://bugzilla.mozilla.org/show_bug.cgi?id=705864
-    output = ''
-    if sys.platform == "win32":
-        # On windows we use tasklist
-        p1 = subprocess.Popen(["tasklist"], stdout=subprocess.PIPE)
-        output = p1.communicate()[0]
-        detected = False
-        for line in output.splitlines():
-            if processName in line:
-                detected = True
-                break
-    else:
-        p1 = subprocess.Popen(["ps", "-ef"], stdout=subprocess.PIPE)
-        p2 = subprocess.Popen(["grep", processName], stdin=p1.stdout, stdout=subprocess.PIPE)
-        p1.stdout.close()
-        output = p2.communicate()[0]
-        detected = False
-        for line in output.splitlines():
-            if "grep %s" % processName in line:
-                continue
-            elif processName in line and not 'defunct' in line:
-                detected = True
-                break
-
-    return detected, output
-
-
-class ProcTest(unittest.TestCase):
-
-    @classmethod
-    def setUpClass(cls):
-        cls.proclaunch = make_proclaunch(here)
-
-    @classmethod
-    def tearDownClass(cls):
-        files = [('proclaunch',),
-                 ('proclaunch.exe',),
-                 ('iniparser', 'dictionary.o'),
-                 ('iniparser', 'iniparser.lib'),
-                 ('iniparser', 'iniparser.o'),
-                 ('iniparser', 'libiniparser.a'),
-                 ('iniparser', 'libiniparser.so.0'),
-                 ]
-        files = [os.path.join(here, *path) for path in files]
-        errors = []
-        for path in files:
-            if os.path.exists(path):
-                try:
-                    os.remove(path)
-                except OSError as e:
-                    errors.append(str(e))
-        if errors:
-            raise OSError("Error(s) encountered tearing down %s.%s:\n%s" % (cls.__module__, cls.__name__, '\n'.join(errors)))
-        del cls.proclaunch
-
-    def test_process_normal_finish(self):
-        """Process is started, runs to completion while we wait for it"""
-
-        p = processhandler.ProcessHandler([self.proclaunch, "process_normal_finish.ini"],
-                                          cwd=here)
-        p.run()
-        p.wait()
-
-        detected, output = check_for_process(self.proclaunch)
-        self.determine_status(detected,
-                              output,
-                              p.proc.returncode,
-                              p.didTimeout)
-
-    def test_process_wait(self):
-        """Process is started runs to completion while we wait indefinitely"""
-
-        p = processhandler.ProcessHandler([self.proclaunch,
-                                          "process_waittimeout_10s.ini"],
-                                          cwd=here)
-        p.run()
-        p.wait()
-
-        detected, output = check_for_process(self.proclaunch)
-        self.determine_status(detected,
-                              output,
-                              p.proc.returncode,
-                              p.didTimeout)
-
-    def test_process_timeout(self):
-        """ Process is started, runs but we time out waiting on it
-            to complete
-        """
-        p = processhandler.ProcessHandler([self.proclaunch, "process_waittimeout.ini"],
-                                          cwd=here)
-        p.run(timeout=10)
-        p.wait()
-
-        detected, output = check_for_process(self.proclaunch)
-        self.determine_status(detected,
-                              output,
-                              p.proc.returncode,
-                              p.didTimeout,
-                              False,
-                              ['returncode', 'didtimeout'])
-
-    def test_process_waittimeout(self):
-        """
-        Process is started, then wait is called and times out.
-        Process is still running and didn't timeout
-        """
-        p = processhandler.ProcessHandler([self.proclaunch,
-                                          "process_waittimeout_10s.ini"],
-                                          cwd=here)
-
-        p.run()
-        p.wait(timeout=5)
-
-        detected, output = check_for_process(self.proclaunch)
-        self.determine_status(detected,
-                              output,
-                              p.proc.returncode,
-                              p.didTimeout,
-                              True,
-                              ())
-
-    def test_process_waitnotimeout(self):
-        """ Process is started, runs to completion before our wait times out
-        """
-        p = processhandler.ProcessHandler([self.proclaunch,
-                                          "process_waittimeout_10s.ini"],
-                                          cwd=here)
-        p.run(timeout=30)
-        p.wait()
-
-        detected, output = check_for_process(self.proclaunch)
-        self.determine_status(detected,
-                              output,
-                              p.proc.returncode,
-                              p.didTimeout)
-
-    def test_process_kill(self):
-        """Process is started, we kill it"""
-
-        p = processhandler.ProcessHandler([self.proclaunch, "process_normal_finish.ini"],
-                                          cwd=here)
-        p.run()
-        p.kill()
-
-        detected, output = check_for_process(self.proclaunch)
-        self.determine_status(detected,
-                              output,
-                              p.proc.returncode,
-                              p.didTimeout)
-
-    def test_process_output_twice(self):
-        """
-        Process is started, then processOutput is called a second time explicitly
-        """
-        p = processhandler.ProcessHandler([self.proclaunch,
-                                          "process_waittimeout_10s.ini"],
-                                          cwd=here)
-
-        p.run()
-        p.processOutput(timeout=5)
-        p.wait()
-
-        detected, output = check_for_process(self.proclaunch)
-        self.determine_status(detected,
-                              output,
-                              p.proc.returncode,
-                              p.didTimeout,
-                              False,
-                              ())
-
-    def determine_status(self,
-                         detected=False,
-                         output='',
-                         returncode=0,
-                         didtimeout=False,
-                         isalive=False,
-                         expectedfail=()):
-        """
-        Use to determine if the situation has failed.
-        Parameters:
-            detected -- value from check_for_process to determine if the process is detected
-            output -- string of data from detected process, can be ''
-            returncode -- return code from process, defaults to 0
-            didtimeout -- True if process timed out, defaults to False
-            isalive -- Use True to indicate we pass if the process exists; however, by default
-                       the test will pass if the process does not exist (isalive == False)
-            expectedfail -- Defaults to [], used to indicate a list of fields that are expected to fail
-        """
-        if 'returncode' in expectedfail:
-            self.assertTrue(returncode, "Detected an unexpected return code of: %s" % returncode)
-        elif not isalive:
-            self.assertTrue(returncode == 0, "Detected non-zero return code of: %d" % returncode)
-
-        if 'didtimeout' in expectedfail:
-            self.assertTrue(didtimeout, "Detected that process didn't time out")
-        else:
-            self.assertTrue(not didtimeout, "Detected that process timed out")
-
-        if isalive:
-            self.assertTrue(detected, "Detected process is not running, process output: %s" % output)
-        else:
-            self.assertTrue(not detected, "Detected process is still running, process output: %s" % output)
-
-if __name__ == '__main__':
-    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/README.md
@@ -0,0 +1,141 @@
+[Mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile)
+is a python tool for creating and managing profiles for Mozilla's
+applications (Firefox, Thunderbird, etc.). In addition to creating profiles,
+mozprofile can install [addons](https://developer.mozilla.org/en/addons)
+and set
+[preferences](https://developer.mozilla.org/En/A_Brief_Guide_to_Mozilla_Preferences).
+Mozprofile can be utilized from the command line or as an API.
+
+
+# Command Line Usage
+
+mozprofile may be used to create profiles, set preferences in
+profiles, or install addons into profiles.
+
+The profile to be operated on may be specified with the `--profile`
+switch. If a profile is not specified, one will be created in a
+temporary directory which will be echoed to the terminal:
+
+    (mozmill)> mozprofile
+    /tmp/tmp4q1iEU.mozrunner
+    (mozmill)> ls /tmp/tmp4q1iEU.mozrunner
+    user.js
+
+To run mozprofile from the command line enter:
+`mozprofile --help` for a list of options.
+
+
+# API Usage
+
+To use mozprofile as an API you can import
+[mozprofile.profile](https://github.com/mozilla/mozbase/tree/master/mozprofile/mozprofile/profile.py)
+and/or the
+[AddonManager](https://github.com/mozilla/mozbase/tree/master/mozprofile/mozprofile/addons.py).
+
+`mozprofile.profile` features a generic `Profile` class.  In addition,
+subclasses `FirefoxProfile` and `ThundebirdProfile` are available
+with preset preferences for those applications.
+
+`mozprofile.profile:Profile`:
+
+    def __init__(self,
+                 profile=None, # Path to the profile
+                 addons=None,  # String of one or list of addons to install
+                 addon_manifests=None,  # Manifest for addons, see http://ahal.ca/blog/2011/bulk-installing-fx-addons/
+                 preferences=None, # Dictionary or class of preferences
+                 locations=None, # locations to proxy
+                 proxy=False, # setup a proxy
+                 restore=True # If true remove all installed addons preferences when cleaning up
+                 ):
+
+    def reset(self):
+        """reset the profile to the beginning state"""
+
+    def set_preferences(self, preferences, filename='user.js'):
+        """Adds preferences dict to profile preferences"""
+
+    def clean_preferences(self):
+        """Removed preferences added by mozrunner."""
+
+    def cleanup(self):
+        """Cleanup operations for the profile."""
+
+
+`mozprofile.addons:AddonManager`:
+
+    def __init__(self, profile):
+        """profile - the path to the profile for which we install addons"""
+
+    def install_addons(self, addons=None, manifests=None):
+        """
+        Installs all types of addons
+        addons - a list of addon paths to install
+        manifest - a list of addon manifests to install
+        """
+
+    @classmethod
+    def get_amo_install_path(self, query):
+        """
+        Return the addon xpi install path for the specified AMO query.
+        See: https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
+        for query documentation.
+        """
+
+    @classmethod
+    def addon_details(cls, addon_path):
+        """
+        returns a dictionary of details about the addon
+        - addon_path : path to the addon directory
+        Returns:
+        {'id':      u'rainbow@colors.org', # id of the addon
+         'version': u'1.4',                # version of the addon
+         'name':    u'Rainbow',            # name of the addon
+         'unpack': False } # whether to unpack the addon
+        """
+
+    def clean_addons(self):
+        """Cleans up addons in the profile."""
+
+
+# Installing Addons
+
+Addons may be installed individually or from a manifest.
+
+Example:
+
+	from mozprofile import FirefoxProfile
+	
+	# create new profile to pass to mozmill/mozrunner
+	profile = FirefoxProfile(addons=["adblock.xpi"])
+
+
+# Setting Preferences
+
+Preferences can be set in several ways:
+
+- using the API: You can pass preferences in to the Profile class's
+  constructor: `obj = FirefoxProfile(preferences=[("accessibility.typeaheadfind.flashBar", 0)])`
+- using a JSON blob file: `mozprofile --preferences myprefs.json`
+- using a `.ini` file: `mozprofile --preferences myprefs.ini`
+- via the command line: `mozprofile --pref key:value --pref key:value [...]`
+
+When setting preferences from  an `.ini` file or the `--pref` switch,
+the value will be interpolated as an integer or a boolean
+(`true`/`false`) if possible.
+
+# Setting Permissions
+
+mozprofile also takes care of adding permissions to the profile.
+See https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/permissions.py
+
+
+# Resources
+
+Other Mozilla programs offer additional and overlapping functionality
+for profiles.  There is also substantive documentation on profiles and
+their management.
+
+- [ProfileManager](https://developer.mozilla.org/en/Profile_Manager) : 
+  XULRunner application for managing profiles. Has a GUI and CLI.
+- [python-profilemanager](http://k0s.org/mozilla/hg/profilemanager/) : python CLI interface similar to ProfileManager
+- profile documentation : http://support.mozilla.com/en-US/kb/Profiles
--- a/testing/mozbase/mozprofile/mozprofile/__init__.py
+++ b/testing/mozbase/mozprofile/mozprofile/__init__.py
@@ -1,17 +1,7 @@
 # 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/.
 
-"""
-To use mozprofile as an API you can import mozprofile.profile_ and/or the AddonManager_.
-
-``mozprofile.profile`` features a generic ``Profile`` class.  In addition,
-subclasses ``FirefoxProfile`` and ``ThundebirdProfile`` are available
-with preset preferences for those applications.
-"""
-
 from profile import *
 from addons import *
 from cli import *
-from prefs import *
-from webapps import *
--- a/testing/mozbase/mozprofile/mozprofile/addons.py
+++ b/testing/mozbase/mozprofile/mozprofile/addons.py
@@ -11,62 +11,58 @@ from distutils import dir_util
 from manifestparser import ManifestParser
 from xml.dom import minidom
 
 # Needed for the AMO's rest API - https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
 AMO_API_VERSION = "1.5"
 
 class AddonManager(object):
     """
-    Handles all operations regarding addons in a profile including: installing and cleaning addons
+    Handles all operations regarding addons including: installing and cleaning addons
     """
 
     def __init__(self, profile):
         """
-        :param profile: the path to the profile for which we install addons
+        profile - the path to the profile for which we install addons
         """
         self.profile = profile
 
         # information needed for profile reset:
         # https://github.com/mozilla/mozbase/blob/270a857328b130860d1b1b512e23899557a3c8f7/mozprofile/mozprofile/profile.py#L93
         self.installed_addons = []
         self.installed_manifests = []
 
         # addons that we've installed; needed for cleanup
         self._addon_dirs = []
 
-        # backup dir for already existing addons
-        self.backup_dir = None
-
     def install_addons(self, addons=None, manifests=None):
         """
         Installs all types of addons
-
-        :param addons: a list of addon paths to install
-        :param manifest: a list of addon manifests to install
+        addons - a list of addon paths to install
+        manifest - a list of addon manifests to install
         """
         # install addon paths
         if addons:
             if isinstance(addons, basestring):
                 addons = [addons]
             self.installed_addons.extend(addons)
             for addon in addons:
                 self.install_from_path(addon)
         # install addon manifests
         if manifests:
             if isinstance(manifests, basestring):
                 manifests = [manifests]
             for manifest in manifests:
                 self.install_from_manifest(manifest)
-            self.installed_manifests.extend(manifests)
+            self.installed_manifests.extended(manifests)
 
     def install_from_manifest(self, filepath):
         """
         Installs addons from a manifest
-        :param filepath: path to the manifest of addons to install
+        filepath - path to the manifest of addons to install
         """
         manifest = ManifestParser()
         manifest.read(filepath)
         addons = manifest.get()
 
         for addon in addons:
             if '://' in addon['path'] or os.path.exists(addon['path']):
                 self.install_from_path(addon['path'])
@@ -81,41 +77,36 @@ class AddonManager(object):
             else:
                 query += 'search/' + addon['name'] + '/default/1'   # this query grabs information on the first addon returned from a search
             install_path = AddonManager.get_amo_install_path(query)
             self.install_from_path(install_path)
 
     @classmethod
     def get_amo_install_path(self, query):
         """
-        Get the addon xpi install path for the specified AMO query.
-
-        :param query: query-documentation_
-
-        .. _query-documentation: https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
+        Return the addon xpi install path for the specified AMO query.
+        See: https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
+        for query documentation.
         """
         response = urllib2.urlopen(query)
         dom = minidom.parseString(response.read())
         for node in dom.getElementsByTagName('install')[0].childNodes:
             if node.nodeType == node.TEXT_NODE:
                 return node.data
 
     @classmethod
     def addon_details(cls, addon_path):
         """
-        Returns a dictionary of details about the addon.
-
-        :param addon_path: path to the addon directory
-
-        Returns::
-
-            {'id':      u'rainbow@colors.org', # id of the addon
-             'version': u'1.4',                # version of the addon
-             'name':    u'Rainbow',            # name of the addon
-             'unpack':  False } # whether to unpack the addon
+        returns a dictionary of details about the addon
+        - addon_path : path to the addon directory
+        Returns:
+        {'id':      u'rainbow@colors.org', # id of the addon
+         'version': u'1.4',                # version of the addon
+         'name':    u'Rainbow',            # name of the addon
+         'unpack':  False } # whether to unpack the addon
         """
 
         # TODO: We don't use the unpack variable yet, but we should: bug 662683
         details = {
             'id': None,
             'unpack': False,
             'name': None,
             'version': None
@@ -156,20 +147,20 @@ class AddonManager(object):
         # turn unpack into a true/false value
         if isinstance(details['unpack'], basestring):
             details['unpack'] = details['unpack'].lower() == 'true'
 
         return details
 
     def install_from_path(self, path, unpack=False):
         """
-        Installs addon from a filepath, url or directory of addons in the profile.
-
-        :param path: url, path to .xpi, or directory of addons
-        :param unpack: whether to unpack unless specified otherwise in the install.rdf
+        Installs addon from a filepath, url
+        or directory of addons in the profile.
+        - path: url, path to .xpi, or directory of addons
+        - unpack: whether to unpack unless specified otherwise in the install.rdf
         """
 
         # if the addon is a url, download it
         # note that this won't work with protocols urllib2 doesn't support
         if '://' in path:
             response = urllib2.urlopen(path)
             fd, path = tempfile.mkstemp(suffix='.xpi')
             os.write(fd, response.read())
@@ -213,45 +204,26 @@ class AddonManager(object):
             assert addon_id, 'The addon id could not be found: %s' % addon
 
             # copy the addon to the profile
             extensions_path = os.path.join(self.profile, 'extensions', 'staged')
             addon_path = os.path.join(extensions_path, addon_id)
             if not unpack and not addon_details['unpack'] and xpifile:
                 if not os.path.exists(extensions_path):
                     os.makedirs(extensions_path)
-                # save existing xpi file to restore later
-                if os.path.exists(addon_path + '.xpi'):
-                    self.backup_dir = self.backup_dir or tempfile.mkdtemp()
-                    shutil.copy(addon_path + '.xpi', self.backup_dir)
                 shutil.copy(xpifile, addon_path + '.xpi')
             else:
-                # save existing dir to restore later
-                if os.path.exists(addon_path):
-                    self.backup_dir = self.backup_dir or tempfile.mkdtemp()
-                    dir_util.copy_tree(addon_path, self.backup_dir, preserve_symlinks=1)
                 dir_util.copy_tree(addon, addon_path, preserve_symlinks=1)
                 self._addon_dirs.append(addon_path)
 
             # remove the temporary directory, if any
             if tmpdir:
                 dir_util.remove_tree(tmpdir)
 
         # remove temporary file, if any
         if tmpfile:
             os.remove(tmpfile)
 
     def clean_addons(self):
         """Cleans up addons in the profile."""
         for addon in self._addon_dirs:
             if os.path.isdir(addon):
                 dir_util.remove_tree(addon)
-        # restore backups
-        if self.backup_dir and os.path.isdir(self.backup_dir):
-            extensions_path = os.path.join(self.profile, 'extensions', 'staged')
-            for backup in os.listdir(self.backup_dir):
-                backup_path = os.path.join(self.backup_dir, backup)
-                addon_path = os.path.join(extensions_path, addon)
-                shutil.move(backup_path, addon_path)
-            if not os.listdir(self.backup_dir):
-                shutil.rmtree(self.backup_dir, ignore_errors=True)
-
-    __del__ = clean_addons
--- a/testing/mozbase/mozprofile/mozprofile/cli.py
+++ b/testing/mozbase/mozprofile/mozprofile/cli.py
@@ -14,17 +14,16 @@ import sys
 from addons import AddonManager
 from optparse import OptionParser
 from prefs import Preferences
 from profile import Profile
 
 __all__ = ['MozProfileCLI', 'cli']
 
 class MozProfileCLI(object):
-    """The Command Line Interface for ``mozprofile``."""
 
     module = 'mozprofile'
 
     def __init__(self, args=sys.argv[1:]):
         self.parser = OptionParser(description=__doc__)
         self.add_options(self.parser)
         (self.options, self.args) = self.parser.parse_args(args)
 
@@ -71,31 +70,25 @@ class MozProfileCLI(object):
                 self.parser.error("Preference must be a key-value pair separated by a ':' (You gave: %s)" % pref)
             cli_prefs.append(pref.split(separator, 1))
 
         # string preferences
         prefs.add(cli_prefs, cast=True)
 
         return prefs()
 
-    def profile(self, restore=False):
-        """create the profile"""
-
-        kwargs = self.profile_args()
-        kwargs['restore'] = restore
-        return Profile(**kwargs)
-
 
 def cli(args=sys.argv[1:]):
-    """ Handles the command line arguments for ``mozprofile`` via ``sys.argv``"""
 
     # process the command line
     cli = MozProfileCLI(args)
 
     # create the profile
-    profile = cli.profile()
+    kwargs = cli.profile_args()
+    kwargs['restore'] = False
+    profile = Profile(**kwargs)
 
     # if no profile was passed in print the newly created profile
     if not cli.options.profile:
         print profile.profile
 
 if __name__ == '__main__':
     cli()
--- a/testing/mozbase/mozprofile/mozprofile/permissions.py
+++ b/testing/mozbase/mozprofile/mozprofile/permissions.py
@@ -18,85 +18,85 @@ import os
 try:
     import sqlite3
 except ImportError:
     from pysqlite2 import dbapi2 as sqlite3
 import urlparse
 
 
 class LocationError(Exception):
-    """Signifies an improperly formed location."""
+    "Signifies an improperly formed location."
 
     def __str__(self):
         s = "Bad location"
         if self.message:
             s += ": %s" % self.message
         return s
 
 
 class MissingPrimaryLocationError(LocationError):
-    """No primary location defined in locations file."""
+    "No primary location defined in locations file."
 
     def __init__(self):
         LocationError.__init__(self, "missing primary location")
 
 
 class MultiplePrimaryLocationsError(LocationError):
-    """More than one primary location defined."""
+    "More than one primary location defined."
 
     def __init__(self):
         LocationError.__init__(self, "multiple primary locations")
 
 
 class DuplicateLocationError(LocationError):
-    """Same location defined twice."""
+    "Same location defined twice."
 
     def __init__(self, url):
         LocationError.__init__(self, "duplicate location: %s" % url)
 
 
 class BadPortLocationError(LocationError):
-    """Location has invalid port value."""
+    "Location has invalid port value."
 
     def __init__(self, given_port):
         LocationError.__init__(self, "bad value for port: %s" % given_port)
         
 
 class LocationsSyntaxError(Exception):
-    """Signifies a syntax error on a particular line in server-locations.txt."""
+    "Signifies a syntax error on a particular line in server-locations.txt."
 
     def __init__(self, lineno, err=None):
         self.err = err
         self.lineno = lineno
 
     def __str__(self):
         s = "Syntax error on line %s" % self.lineno
         if self.err:
             s += ": %s." % self.err
         else:
             s += "."
         return s
 
 
 class Location(object):
-    """Represents a location line in server-locations.txt."""
+    "Represents a location line in server-locations.txt."
 
     attrs = ('scheme', 'host', 'port')
 
     def __init__(self, scheme, host, port, options):
         for attr in self.attrs:
             setattr(self, attr, locals()[attr])
         self.options = options
         try:
             int(self.port)
         except ValueError:
             raise BadPortLocationError(self.port)
 
     def isEqual(self, location):
-        """compare scheme://host:port, but ignore options"""
+        "compare scheme://host:port, but ignore options"
         return len([i for i in self.attrs if getattr(self, i) == getattr(location, i)]) == len(self.attrs)
 
     __eq__ = isEqual
 
     def url(self):
         return '%s://%s:%s' % (self.scheme, self.host, self.port)
 
     def __str__(self):
@@ -135,23 +135,24 @@ class ServerLocations(object):
 
     def add_host(self, host, port='80', scheme='http', options='privileged'):
         if isinstance(options, basestring):
             options = options.split(',')
         self.add(Location(scheme, host, port, options))
 
     def read(self, filename, check_for_primary=True):
         """
-        Reads the file and adds all valid locations to the ``self._locations`` array.
+        Reads the file (in the format of server-locations.txt) and add all
+        valid locations to the self._locations array.
 
-        :param filename: in the format of server-locations.txt_
-        :param check_for_primary: if True, a ``MissingPrimaryLocationError`` exception is raised if no primary is found
+        If check_for_primary is True, a MissingPrimaryLocationError
+        exception is raised if no primary is found.
 
-        .. _server-locations.txt: http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt
-
+        This format:
+        http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt
         The only exception is that the port, if not defined, defaults to 80 or 443.
 
         FIXME: Shouldn't this default to the protocol-appropriate port?  Is
         there any reason to have defaults at all?
         """
 
         locationFile = codecs.open(filename, "r", "UTF-8")
         lineno = 0
@@ -201,18 +202,16 @@ class ServerLocations(object):
             raise LocationsSyntaxError(lineno + 1,
                                        MissingPrimaryLocationError())
 
         if self.add_callback:
             self.add_callback(new_locations)
 
 
 class Permissions(object):
-    """Allows handling of permissions for ``mozprofile``"""
-
     _num_permissions = 0
 
     def __init__(self, profileDir, locations=None):
         self._profileDir = profileDir
         self._locations = ServerLocations(add_callback=self.write_db)
         if locations:
             if isinstance(locations, ServerLocations):
                 self._locations = locations
@@ -227,17 +226,16 @@ class Permissions(object):
                 self._locations.read(locations)
 
     def write_db(self, locations):
         """write permissions to the sqlite database"""
 
         # Open database and create table
         permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sqlite"))
         cursor = permDB.cursor();
-        cursor.execute("PRAGMA schema_version = 3;")
         # SQL copied from
         # http://mxr.mozilla.org/mozilla-central/source/extensions/cookie/nsPermissionManager.cpp
         cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
            id INTEGER PRIMARY KEY,
            host TEXT,
            type TEXT,
            permission INTEGER,
            expireType INTEGER,
--- a/testing/mozbase/mozprofile/mozprofile/prefs.py
+++ b/testing/mozbase/mozprofile/mozprofile/prefs.py
@@ -1,23 +1,19 @@
 # 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/.
 
 """
 user preferences
 """
 
-__all__ = ('PreferencesReadError', 'Preferences')
-
 import os
 import re
-import tokenize
 from ConfigParser import SafeConfigParser as ConfigParser
-from StringIO import StringIO
 
 try:
     import json
 except ImportError:
     import simplejson as json
 
 class PreferencesReadError(Exception):
     """read error for prefrences files"""
@@ -28,43 +24,38 @@ class Preferences(object):
 
     def __init__(self, prefs=None):
         self._prefs = []
         if prefs:
             self.add(prefs)
 
     def add(self, prefs, cast=False):
         """
-        :param prefs:
-        :param cast: whether to cast strings to value, e.g. '1' -> 1
+        - cast: whether to cast strings to value, e.g. '1' -> 1
         """
         # wants a list of 2-tuples
         if isinstance(prefs, dict):
             prefs = prefs.items()
         if cast:
             prefs = [(i, self.cast(j)) for i, j in prefs]
         self._prefs += prefs
 
     def add_file(self, path):
-        """a preferences from a file
-        
-        :param path:
-        """
+        """a preferences from a file"""
         self.add(self.read(path))
 
     def __call__(self):
         return self._prefs
 
     @classmethod
     def cast(cls, value):
         """
         interpolate a preference from a string
         from the command line or from e.g. an .ini file, there is no good way to denote
         what type the preference value is, as natively it is a string
-
         - integers will get cast to integers
         - true/false will get cast to True/False
         - anything enclosed in single quotes will be treated as a string with the ''s removed from both sides
         """
 
         if not isinstance(value, basestring):
             return value # no op
         quote = "'"
@@ -155,74 +146,65 @@ class Preferences(object):
         return prefs
 
     @classmethod
     def read_prefs(cls, path, pref_setter='user_pref'):
         """read preferences from (e.g.) prefs.js"""
 
         comment = re.compile('/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/', re.MULTILINE)
 
-        marker = '##//' # magical marker
+        token = '##//' # magical token
         lines = [i.strip() for i in file(path).readlines() if i.strip()]
         _lines = []
         for line in lines:
-            if line.startswith(('#', '//')):
+            if line.startswith('#'):
                 continue
             if '//' in line:
-                line = line.replace('//', marker)
+                line = line.replace('//', token)
             _lines.append(line)
         string = '\n'.join(_lines)
         string = re.sub(comment, '', string)
 
-        # skip trailing comments
-        processed_tokens = []
-        f_obj = StringIO(string)
-        for token in tokenize.generate_tokens(f_obj.readline):
-            if token[0] == tokenize.COMMENT:
-                continue
-            processed_tokens.append(token[:2]) # [:2] gets around http://bugs.python.org/issue9974
-        string = tokenize.untokenize(processed_tokens)
-
         retval = []
         def pref(a, b):
             retval.append((a, b))
         lines = [i.strip().rstrip(';') for i in string.split('\n') if i.strip()]
 
         _globals = {'retval': retval, 'true': True, 'false': False}
         _globals[pref_setter] = pref
         for line in lines:
             try:
                 eval(line, _globals, {})
             except SyntaxError:
                 print line
                 raise
 
-        # de-magic the marker
+        # de-magic the token
         for index, (key, value) in enumerate(retval):
-            if isinstance(value, basestring) and marker in value:
-                retval[index] = (key, value.replace(marker, '//'))
+            if isinstance(value, basestring) and token in value:
+                retval[index] = (key, value.replace(token, '//'))
 
         return retval
 
     @classmethod
-    def write(cls, _file, prefs, pref_string='user_pref("%s", %s);'):
+    def write(_file, prefs, pref_string='user_pref("%s", %s);'):
         """write preferences to a file"""
 
         if isinstance(_file, basestring):
             f = file(_file, 'w')
         else:
             f = _file
 
         if isinstance(prefs, dict):
             prefs = prefs.items()
 
         for key, value in prefs:
             if value is True:
                 print >> f, pref_string % (key, 'true')
             elif value is False:
                 print >> f, pref_string % (key, 'false')
             elif isinstance(value, basestring):
-                print >> f, pref_string % (key, repr(str(value)))
+                print >> f, pref_string % (key, repr(string(value)))
             else:
                 print >> f, pref_string % (key, value) # should be numeric!
 
         if isinstance(_file, basestring):
             f.close()
--- a/testing/mozbase/mozprofile/mozprofile/profile.py
+++ b/testing/mozbase/mozprofile/mozprofile/profile.py
@@ -2,44 +2,39 @@
 # 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/.
 
 __all__ = ['Profile', 'FirefoxProfile', 'ThunderbirdProfile']
 
 import os
 import time
 import tempfile
-import types
 import uuid
 from addons import AddonManager
 from permissions import Permissions
-from shutil import copytree, rmtree
-from webapps import WebappCollection
+from shutil import rmtree
 
 try:
-    import json
+    import simplejson
 except ImportError:
-    import simplejson as json
+    import json as simplejson
 
 class Profile(object):
     """Handles all operations regarding profile. Created new profiles, installs extensions,
     sets preferences and handles cleanup."""
 
-    def __init__(self, profile=None, addons=None, addon_manifests=None, apps=None,
-                 preferences=None, locations=None, proxy=None, restore=True):
-        """
-        :param profile: Path to the profile
-        :param addons: String of one or list of addons to install
-        :param addon_manifests: Manifest for addons, see http://ahal.ca/blog/2011/bulk-installing-fx-addons/
-        :param apps: Dictionary or class of webapps to install
-        :param preferences: Dictionary or class of preferences
-        :param locations: locations to proxy
-        :param proxy: setup a proxy - dict of server-loc,server-port,ssl-port
-        :param restore: If true remove all installed addons preferences when cleaning up
-        """
+    def __init__(self,
+                 profile=None, # Path to the profile
+                 addons=None,  # String of one or list of addons to install
+                 addon_manifests=None,  # Manifest for addons, see http://ahal.ca/blog/2011/bulk-installing-fx-addons/
+                 preferences=None, # Dictionary or class of preferences
+                 locations=None, # locations to proxy
+                 proxy=None, # setup a proxy - dict of server-loc,server-port,ssl-port
+                 restore=True # If true remove all installed addons preferences when cleaning up
+                 ):
 
         # if true, remove installed addons/prefs afterwards
         self.restore = restore
 
         # prefs files written to
         self.written_prefs = set()
 
         # our magic markers
@@ -80,20 +75,16 @@ class Profile(object):
         prefs_js, user_js = self.permissions.network_prefs(proxy)
         self.set_preferences(prefs_js, 'prefs.js')
         self.set_preferences(user_js)
 
         # handle addon installation
         self.addon_manager = AddonManager(self.profile)
         self.addon_manager.install_addons(addons, addon_manifests)
 
-        # handle webapps
-        self.webapps = WebappCollection(profile=self.profile, apps=apps)
-        self.webapps.update_manifests()
-
     def exists(self):
         """returns whether the profile exists or not"""
         return os.path.exists(self.profile)
 
     def reset(self):
         """
         reset the profile to the beginning state
         """
@@ -104,68 +95,45 @@ class Profile(object):
             profile = self.profile
         self.__init__(profile=profile,
                       addons=self.addon_manager.installed_addons,
                       addon_manifests=self.addon_manager.installed_manifests,
                       preferences=self._preferences,
                       locations=self._locations,
                       proxy = self._proxy)
 
-    @classmethod
-    def clone(cls, path_from, path_to=None, **kwargs):
-        """Instantiate a temporary profile via cloning
-        - path: path of the basis to clone
-        - kwargs: arguments to the profile constructor
-        """
-        if not path_to:
-            tempdir = tempfile.mkdtemp() # need an unused temp dir name
-            rmtree(tempdir) # copytree requires that dest does not exist
-            path_to = tempdir
-        copytree(path_from, path_to)
-
-        def cleanup_clone(fn):
-            """Deletes a cloned profile when restore is True"""
-            def wrapped(self):
-                fn(self)
-                if self.restore and os.path.exists(self.profile):
-                        rmtree(self.profile, onerror=self._cleanup_error)
-            return wrapped
-
-        c = cls(path_to, **kwargs)
-        c.__del__ = c.cleanup = types.MethodType(cleanup_clone(cls.cleanup), c)
-        return c
-
     def create_new_profile(self):
         """Create a new clean profile in tmp which is a simple empty folder"""
         profile = tempfile.mkdtemp(suffix='.mozrunner')
         return profile
 
 
     ### methods for preferences
 
     def set_preferences(self, preferences, filename='user.js'):
         """Adds preferences dict to profile preferences"""
 
+
         # append to the file
         prefs_file = os.path.join(self.profile, filename)
         f = open(prefs_file, 'a')
 
         if preferences:
 
             # note what files we've touched
             self.written_prefs.add(filename)
 
 
             if isinstance(preferences, dict):
                 # order doesn't matter
                 preferences = preferences.items()
 
             # write the preferences
             f.write('\n%s\n' % self.delimeters[0])
-            _prefs = [(json.dumps(k), json.dumps(v) )
+            _prefs = [(simplejson.dumps(k), simplejson.dumps(v) )
                       for k, v in preferences]
             for _pref in _prefs:
                 f.write('user_pref(%s, %s);\n' % _pref)
             f.write('%s\n' % self.delimeters[1])
         f.close()
 
     def pop_preferences(self, filename):
         """
@@ -246,60 +214,51 @@ class Profile(object):
         if self.restore:
             if self.create_new:
                 if os.path.exists(self.profile):
                     rmtree(self.profile, onerror=self._cleanup_error)
             else:
                 self.clean_preferences()
                 self.addon_manager.clean_addons()
                 self.permissions.clean_db()
-                self.webapps.clean()
 
     __del__ = cleanup
 
 class FirefoxProfile(Profile):
     """Specialized Profile subclass for Firefox"""
     preferences = {# Don't automatically update the application
                    'app.update.enabled' : False,
                    # Don't restore the last open set of tabs if the browser has crashed
                    'browser.sessionstore.resume_from_crash': False,
                    # Don't check for the default web browser
                    'browser.shell.checkDefaultBrowser' : False,
                    # Don't warn on exit when multiple tabs are open
                    'browser.tabs.warnOnClose' : False,
                    # Don't warn when exiting the browser
                    'browser.warnOnQuit': False,
-                   # Don't send Firefox health reports to the production server
-                   'datareporting.healthreport.documentServerURI' : 'http://%(server)s/healthreport/',
                    # Only install add-ons from the profile and the application scope
                    # Also ensure that those are not getting disabled.
                    # see: https://developer.mozilla.org/en/Installing_extensions
                    'extensions.enabledScopes' : 5,
                    'extensions.autoDisableScopes' : 10,
                    # Don't install distribution add-ons from the app folder
                    'extensions.installDistroAddons' : False,
                    # Dont' run the add-on compatibility check during start-up
                    'extensions.showMismatchUI' : False,
                    # Don't automatically update add-ons
                    'extensions.update.enabled'    : False,
                    # Don't open a dialog to show available add-on updates
                    'extensions.update.notifyUser' : False,
+                   # Suppress automatic safe mode after crashes
+                   'toolkit.startup.max_resumed_crashes' : -1,
                    # Enable test mode to run multiple tests in parallel
                    'focusmanager.testmode' : True,
-                   # Suppress delay for main action in popup notifications
-                   'security.notification_enable_delay' : 0,
-                   # Suppress automatic safe mode after crashes
-                   'toolkit.startup.max_resumed_crashes' : -1,
-                   # Don't report telemetry information
-                   'toolkit.telemetry.enabled' : False,
-                   'toolkit.telemetry.enabledPreRelease' : False,
                    }
 
 class ThunderbirdProfile(Profile):
-    """Specialized Profile subclass for Thunderbird"""
     preferences = {'extensions.update.enabled'    : False,
                    'extensions.update.notifyUser' : False,
                    'browser.shell.checkDefaultBrowser' : False,
                    'browser.tabs.warnOnClose' : False,
                    'browser.warnOnQuit': False,
                    'browser.sessionstore.resume_from_crash': False,
                    # prevents the 'new e-mail address' wizard on new profile
                    'mail.provider.enabled': False,
deleted file mode 100644
--- a/testing/mozbase/mozprofile/mozprofile/webapps.py
+++ /dev/null
@@ -1,279 +0,0 @@
-# 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/.
-
-"""
-Handles installing open webapps (https://developer.mozilla.org/en-US/docs/Apps)
-to a profile. A webapp object is a dict that contains some metadata about
-the webapp and must at least include a name, description and manifestURL.
-
-Each webapp has a manifest (https://developer.mozilla.org/en-US/docs/Apps/Manifest).
-Additionally there is a separate json manifest that keeps track of the installed
-webapps, their manifestURLs and their permissions.
-"""
-
-__all__ = ["Webapp", "WebappCollection", "WebappFormatException", "APP_STATUS_NOT_INSTALLED",
-           "APP_STATUS_INSTALLED", "APP_STATUS_PRIVILEGED", "APP_STATUS_CERTIFIED"]
-
-from string import Template
-import os
-import shutil
-
-try:
-    import json
-except ImportError:
-    import simplejson as json
-
-# from http://hg.mozilla.org/mozilla-central/file/add0b94c2c0b/caps/idl/nsIPrincipal.idl#l163
-APP_STATUS_NOT_INSTALLED = 0
-APP_STATUS_INSTALLED     = 1
-APP_STATUS_PRIVILEGED    = 2
-APP_STATUS_CERTIFIED     = 3
-
-class WebappFormatException(Exception):
-    """thrown for invalid webapp objects"""
-
-class Webapp(dict):
-    """A webapp definition"""
-
-    required_keys = ('name', 'description', 'manifestURL')
-
-    def __init__(self, *args, **kwargs):
-        try:
-            dict.__init__(self, *args, **kwargs)
-        except (TypeError, ValueError):
-            raise WebappFormatException("Webapp object should be an instance of type 'dict'")
-        self.validate()
-
-    def __eq__(self, other):
-        """Webapps are considered equal if they have the same name"""
-        if not isinstance(other, self.__class__):
-            return False
-        return self['name'] == other['name']
-
-    def __ne__(self, other):
-        """Webapps are considered not equal if they have different names"""
-        return not self.__eq__(other)
-
-    def validate(self):
-        # TODO some keys are required if another key has a certain value
-        for key in self.required_keys:
-            if key not in self:
-                raise WebappFormatException("Webapp object missing required key '%s'" % key)
-
-
-class WebappCollection(object):
-    """A list-like object that collects webapps and updates the webapp manifests"""
-
-    json_template = Template(""""$name": {
-  "origin": "$origin",
-  "installOrigin": "$origin",
-  "receipt": null,
-  "installTime": 132333986000,
-  "manifestURL": "$manifestURL",
-  "localId": $localId,
-  "id": "$name",
-  "appStatus": $appStatus,
-  "csp": "$csp"
-}""")
-
-    manifest_template = Template("""{
-  "name": "$name",
-  "csp": "$csp",
-  "description": "$description",
-  "launch_path": "/",
-  "developer": {
-    "name": "Mozilla",
-    "url": "https://mozilla.org/"
-  },
-  "permissions": [
-  ],
-  "locales": {
-    "en-US": {
-      "name": "$name",
-      "description": "$description"
-    }
-  },
-  "default_locale": "en-US",
-  "icons": {
-  }
-}
-""")
-
-    def __init__(self, profile, apps=None, json_template=None, manifest_template=None):
-        """
-        :param profile: the file path to a profile
-        :param apps: [optional] a list of webapp objects or file paths to json files describing webapps
-        :param json_template: [optional] string template describing the webapp json format
-        :param manifest_template: [optional] string template describing the webapp manifest format
-        """
-        if not isinstance(profile, basestring):
-            raise TypeError("Must provide path to a profile, received '%s'" % type(profile))
-        self.profile = profile
-        self.webapps_dir = os.path.join(self.profile, 'webapps')
-        self.backup_dir = os.path.join(self.profile, '.mozprofile_backup', 'webapps')
-
-        self._apps = []
-        self._installed_apps = []
-        if apps:
-            if not isinstance(apps, (list, set, tuple)):
-                apps = [apps]
-
-            for app in apps:
-                if isinstance(app, basestring) and os.path.isfile(app):
-                    self.extend(self.read_json(app))
-                else:
-                    self.append(app)
-
-        self.json_template = json_template or self.json_template
-        self.manifest_template = manifest_template or self.manifest_template
-
-    def __getitem__(self, index):
-        return self._apps.__getitem__(index)
-
-    def __setitem__(self, index, value):
-        return self._apps.__setitem__(index, Webapp(value))
-
-    def __delitem__(self, index):
-        return self._apps.__delitem__(index)
-
-    def __len__(self):
-        return self._apps.__len__()
-
-    def __contains__(self, value):
-        return self._apps.__contains__(Webapp(value))
-
-    def append(self, value):
-        return self._apps.append(Webapp(value))
-
-    def insert(self, index, value):
-        return self._apps.insert(index, Webapp(value))
-
-    def extend(self, values):
-        return self._apps.extend([Webapp(v) for v in values])
-
-    def remove(self, value):
-        return self._apps.remove(Webapp(value))
-
-    def _write_webapps_json(self, apps):
-        contents = []
-        for app in apps:
-            contents.append(self.json_template.substitute(app))
-        contents = '{\n' + ',\n'.join(contents) + '\n}\n'
-        webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json')
-        webapps_json_file = open(webapps_json_path, "w")
-        webapps_json_file.write(contents)
-        webapps_json_file.close()
-
-    def _write_webapp_manifests(self, write_apps=[], remove_apps=[]):
-        # Write manifests for installed apps
-        for app in write_apps:
-            manifest_dir = os.path.join(self.webapps_dir, app['name'])
-            manifest_path = os.path.join(manifest_dir, 'manifest.webapp')
-            if not os.path.isfile(manifest_path):
-                if not os.path.isdir(manifest_dir):
-                    os.mkdir(manifest_dir)
-                manifest = self.manifest_template.substitute(app)
-                manifest_file = open(manifest_path, "a")
-                manifest_file.write(manifest)
-                manifest_file.close()
-        # Remove manifests for removed apps
-        for app in remove_apps:
-            self._installed_apps.remove(app)
-            manifest_dir = os.path.join(self.webapps_dir, app['name'])
-            if os.path.isdir(manifest_dir):
-                shutil.rmtree(manifest_dir)
-
-    def update_manifests(self):
-        """Updates the webapp manifests with the webapps represented in this collection
-
-        If update_manifests is called a subsequent time, there could have been apps added or
-        removed to the collection in the interim. The manifests will be adjusted accordingly
-        """
-        apps_to_install = [app for app in self._apps if app not in self._installed_apps]
-        apps_to_remove = [app for app in self._installed_apps if app not in self._apps]
-        if apps_to_install == apps_to_remove == []:
-            # nothing to do
-            return
-
-        if not os.path.isdir(self.webapps_dir):
-            os.makedirs(self.webapps_dir)
-        elif not self._installed_apps:
-            shutil.copytree(self.webapps_dir, self.backup_dir)
-
-        webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json')
-        webapps_json = []
-        if os.path.isfile(webapps_json_path):
-            webapps_json = self.read_json(webapps_json_path, description="description")
-            webapps_json = [a for a in webapps_json if a not in apps_to_remove]
-
-        # Iterate over apps already in webapps.json to determine the starting local
-        # id and to ensure apps are properly formatted
-        start_id = 1
-        for local_id, app in enumerate(webapps_json):
-            app['localId'] = local_id + 1
-            start_id += 1
-            if not app.get('csp'):
-                app['csp'] = ''
-            if not app.get('appStatus'):
-                app['appStatus'] = 3
-
-        # Append apps_to_install to the pre-existent apps
-        for local_id, app in enumerate(apps_to_install):
-            app['localId'] = local_id + start_id
-            # ignore if it's already installed
-            if app in webapps_json:
-                start_id -= 1
-                continue
-            webapps_json.append(app)
-            self._installed_apps.append(app)
-
-        # Write the full contents to webapps.json
-        self._write_webapps_json(webapps_json)
-
-        # Create/remove manifest file for each app.
-        self._write_webapp_manifests(apps_to_install, apps_to_remove)
-
-    def clean(self):
-        """Remove all webapps that were installed and restore profile to previous state"""
-        if self._installed_apps and os.path.isdir(self.webapps_dir):
-            shutil.rmtree(self.webapps_dir)
-
-        if os.path.isdir(self.backup_dir):
-            shutil.copytree(self.backup_dir, self.webapps_dir)
-            shutil.rmtree(self.backup_dir)
-
-        self._apps = []
-        self._installed_apps = []
-
-    @classmethod
-    def read_json(cls, path, **defaults):
-        """Reads a json file which describes a set of webapps. The json format is either a
-        dictionary where each key represents the name of a webapp (e.g B2G format) or a list
-        of webapp objects.
-
-        :param path: Path to a json file defining webapps
-        :param defaults: Default key value pairs added to each webapp object if key doesn't exist
-
-        Returns a list of Webapp objects
-        """
-        f = open(path, 'r')
-        app_json = json.load(f)
-        f.close()
-
-        apps = []
-        if isinstance(app_json, dict):
-            for k, v in app_json.iteritems():
-                v['name'] = k
-                apps.append(v)
-        else:
-            apps = app_json
-            if not isinstance(apps, list):
-                apps = [apps]
-
-        ret = []
-        for app in apps:
-            d = defaults.copy()
-            d.update(app)
-            ret.append(Webapp(**d))
-        return ret
--- a/testing/mozbase/mozprofile/setup.py
+++ b/testing/mozbase/mozprofile/setup.py
@@ -1,36 +1,44 @@
 # 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 os
 import sys
 from setuptools import setup
 
-PACKAGE_VERSION = '0.5'
+PACKAGE_VERSION = '0.4'
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
 deps = ["ManifestDestiny >= 0.5.4"]
 # version-dependent dependencies
 try:
     import json
 except ImportError:
     deps.append('simplejson')
 try:
     import sqlite3
 except ImportError:
     deps.append('pysqlite')
 
 
+# take description from README
+here = os.path.dirname(os.path.abspath(__file__))
+try:
+    description = file(os.path.join(here, 'README.md')).read()
+except (OSError, IOError):
+    description = ''
+
 setup(name='mozprofile',
       version=PACKAGE_VERSION,
-      description="Library to create and modify Mozilla application profiles",
-      long_description="see http://mozbase.readthedocs.org/",
+      description="Handling of Mozilla Gecko based application profiles",
+      long_description=description,
       classifiers=['Environment :: Console',
                    'Intended Audience :: Developers',
                    'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
                    'Natural Language :: English',
                    'Operating System :: OS Independent',
                    'Programming Language :: Python',
                    'Topic :: Software Development :: Libraries :: Python Modules',
                    ],
deleted file mode 100755
--- a/testing/mozbase/mozprofile/tests/bug785146.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env python
-
-# 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 os
-import shutil
-try:
-    import sqlite3
-except ImportError:
-    from pysqlite2 import dbapi2 as sqlite3
-import tempfile
-import unittest
-from mozprofile.permissions import Permissions
-
-class PermissionsTest(unittest.TestCase):
-
-    locations = """http://mochi.test:8888  primary,privileged
-http://127.0.0.1:80             noxul
-http://127.0.0.1:8888           privileged
-"""
-
-    profile_dir = None
-    locations_file = None
-
-    def setUp(self):
-        self.profile_dir = tempfile.mkdtemp()
-        self.locations_file = tempfile.NamedTemporaryFile()
-        self.locations_file.write(self.locations)
-        self.locations_file.flush()
-
-    def tearDown(self):
-        if self.profile_dir:
-            shutil.rmtree(self.profile_dir)
-        if self.locations_file:
-            self.locations_file.close()
-
-    def test_schema_version(self):
-        perms = Permissions(self.profile_dir, self.locations_file.name)
-        perms_db_filename = os.path.join(self.profile_dir, 'permissions.sqlite')
-        perms.write_db(self.locations_file)
-
-        stmt = 'PRAGMA schema_version;'
-
-        con = sqlite3.connect(perms_db_filename)
-        cur = con.cursor()
-        cur.execute(stmt)
-        entries = cur.fetchall()
-
-        schema_version = entries[0][0]
-        self.assertEqual(schema_version, 3)
-
-if __name__ == '__main__':
-    unittest.main()
deleted file mode 100644
--- a/testing/mozbase/mozprofile/tests/files/prefs_with_comments.js
+++ /dev/null
@@ -1,6 +0,0 @@
-# A leading comment
-user_pref("browser.startup.homepage", "http://planet.mozilla.org"); # A trailing comment
-user_pref("zoom.minPercent", 30);
-// Another leading comment
-user_pref("zoom.maxPercent", 300); // Another trailing comment
-user_pref("webgl.verbose", "false");
deleted file mode 100644
--- a/testing/mozbase/mozprofile/tests/files/webapps1.json
+++ /dev/null
@@ -1,50 +0,0 @@
-[{ "name": "http_example_org",
-   "csp": "",
-   "origin": "http://example.org",
-   "manifestURL": "http://example.org/manifest.webapp",
-   "description": "http://example.org App",
-   "appStatus": 1
- },
- { "name": "https_example_com",
-   "csp": "",
-   "origin": "https://example.com",
-   "manifestURL": "https://example.com/manifest.webapp",
-   "description": "https://example.com App",
-   "appStatus": 1
- },
- { "name": "http_test1_example_org",
-   "csp": "",
-   "origin": "http://test1.example.org",
-   "manifestURL": "http://test1.example.org/manifest.webapp",
-   "description": "http://test1.example.org App",
-   "appStatus": 1
- },
- { "name": "http_test1_example_org_8000",
-   "csp": "",
-   "origin": "http://test1.example.org:8000",
-   "manifestURL": "http://test1.example.org:8000/manifest.webapp",
-   "description": "http://test1.example.org:8000 App",
-   "appStatus": 1
- },
- { "name": "http_sub1_test1_example_org",
-   "csp": "",
-   "origin": "http://sub1.test1.example.org",
-   "manifestURL": "http://sub1.test1.example.org/manifest.webapp",
-   "description": "http://sub1.test1.example.org App",
-   "appStatus": 1
- },
- { "name": "https_example_com_privileged",
-   "csp": "",
-   "origin": "https://example.com",
-   "manifestURL": "https://example.com/manifest_priv.webapp",
-   "description": "https://example.com Privileged App",
-   "appStatus": 2
- },
- { "name": "https_example_com_certified",
-   "csp": "",
-   "origin": "https://example.com",
-   "manifestURL": "https://example.com/manifest_cert.webapp",
-   "description": "https://example.com Certified App",
-   "appStatus": 3
- }
-]
deleted file mode 100644
--- a/testing/mozbase/mozprofile/tests/files/webapps2.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
-    "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": 3
-    },
-    "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": 1
-    },
-    "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": 2
-    },
-    "https_a_domain_certified": {
-      "csp": "",
-      "origin": "https://acertified.com",
-      "manifestURL": "https://acertified.com/manifest.webapp",
-      "description": "https://acertified.com certified app",
-      "appStatus": 3
-    },
-    "https_a_domain_privileged": {
-      "csp": "",
-      "origin": "https://aprivileged.com",
-      "manifestURL": "https://aprivileged.com/manifest.webapp",
-      "description": "https://aprivileged.com privileged app ",
-      "appStatus": 2
-    }
-}
--- a/testing/mozbase/mozprofile/tests/manifest.ini
+++ b/testing/mozbase/mozprofile/tests/manifest.ini
@@ -1,9 +1,6 @@
 [addonid.py]
 [server_locations.py]
 [test_preferences.py]
 [permissions.py]
 [bug758250.py]
 [test_nonce.py]
-[bug785146.py]
-[test_clone_cleanup.py]
-[test_webapps.py]
deleted file mode 100644
--- a/testing/mozbase/mozprofile/tests/test_clone_cleanup.py
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/env python
-
-# 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 os
-import tempfile
-import unittest
-from mozprofile.profile import Profile
-
-class CloneCleanupTest(unittest.TestCase):
-    """
-    test cleanup logic for the clone functionality
-    see https://bugzilla.mozilla.org/show_bug.cgi?id=642843
-    """
-
-    def setUp(self):
-        # make a profile with one preference
-        path = tempfile.mktemp()
-        self.profile = Profile(path,
-                          preferences={'foo': 'bar'},
-                          restore=False)
-        user_js = os.path.join(self.profile.profile, 'user.js')
-        self.assertTrue(os.path.exists(user_js))
-
-    def test_restore_true(self):
-        # make a clone of this profile with restore=True
-        clone = Profile.clone(self.profile.profile, restore=True)
-
-        clone.cleanup()
-
-        # clone should be deleted
-        self.assertFalse(os.path.exists(clone.profile))
-
-    def test_restore_false(self):
-        # make a clone of this profile with restore=False
-        clone = Profile.clone(self.profile.profile, restore=False)
-
-        clone.cleanup()
-
-        # clone should still be around on the filesystem
-        self.assertTrue(os.path.exists(clone.profile))
-
-
-if __name__ == '__main__':
-    unittest.main()
-
--- a/testing/mozbase/mozprofile/tests/test_preferences.py
+++ b/testing/mozbase/mozprofile/tests/test_preferences.py
@@ -1,107 +1,95 @@
 #!/usr/bin/env python
 
-# 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 os
 import shutil
+import subprocess
 import tempfile
 import unittest
-from mozprofile.cli import MozProfileCLI
 from mozprofile.prefs import Preferences
 from mozprofile.profile import Profile
 
-here = os.path.dirname(os.path.abspath(__file__))
-
 class PreferencesTest(unittest.TestCase):
-    """test mozprofile preference handling"""
+    """test mozprofile"""
 
     def run_command(self, *args):
         """
-        invokes mozprofile command line via the CLI factory
-        - args : command line arguments (equivalent of sys.argv[1:])
+        runs mozprofile;
+        returns (stdout, stderr, code)
         """
-
-        # instantiate the factory
-        cli = MozProfileCLI(list(args))
-
-        # create the profile
-        profile = cli.profile()
-
-        # return path to profile
-        return profile.profile
+        process = subprocess.Popen(args,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        stdout, stderr = process.communicate()
+        stdout = stdout.strip()
+        stderr = stderr.strip()
+        return stdout, stderr, process.returncode
 
     def compare_generated(self, _prefs, commandline):
         """
         writes out to a new profile with mozprofile command line
         reads the generated preferences with prefs.py
         compares the results
         cleans up
         """
-        profile = self.run_command(*commandline)
+        profile, stderr, code = self.run_command(*commandline)
         prefs_file = os.path.join(profile, 'user.js')
         self.assertTrue(os.path.exists(prefs_file))
         read = Preferences.read_prefs(prefs_file)
         if isinstance(_prefs, dict):
             read = dict(read)
         self.assertEqual(_prefs, read)
         shutil.rmtree(profile)
 
     def test_basic_prefs(self):
-        """test setting a pref from the command line entry point"""
-
         _prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"}
-        commandline = []
+        commandline = ["mozprofile"]
         _prefs = _prefs.items()
         for pref, value in _prefs:
             commandline += ["--pref", "%s:%s" % (pref, value)]
         self.compare_generated(_prefs, commandline)
 
     def test_ordered_prefs(self):
         """ensure the prefs stay in the right order"""
         _prefs = [("browser.startup.homepage", "http://planet.mozilla.org/"),
                   ("zoom.minPercent", 30),
                   ("zoom.maxPercent", 300),
                   ("webgl.verbose", 'false')]
-        commandline = []
+        commandline = ["mozprofile"]
         for pref, value in _prefs:
             commandline += ["--pref", "%s:%s" % (pref, value)]
         _prefs = [(i, Preferences.cast(j)) for i, j in _prefs]
         self.compare_generated(_prefs, commandline)
 
     def test_ini(self):
 
         # write the .ini file
         _ini = """[DEFAULT]
 browser.startup.homepage = http://planet.mozilla.org/
 
 [foo]
 browser.startup.homepage = http://github.com/
 """
-        try:
-            fd, name = tempfile.mkstemp(suffix='.ini')
-            os.write(fd, _ini)
-            os.close(fd)
-            commandline = ["--preferences", name]
+        fd, name = tempfile.mkstemp(suffix='.ini')
+        os.write(fd, _ini)
+        os.close(fd)
+        commandline = ["mozprofile", "--preferences", name]
 
-            # test the [DEFAULT] section
-            _prefs = {'browser.startup.homepage': 'http://planet.mozilla.org/'}
-            self.compare_generated(_prefs, commandline)
+        # test the [DEFAULT] section
+        _prefs = {'browser.startup.homepage': 'http://planet.mozilla.org/'}
+        self.compare_generated(_prefs, commandline)
 
-            # test a specific section
-            _prefs = {'browser.startup.homepage': 'http://github.com/'}
-            commandline[-1] = commandline[-1] + ':foo'
-            self.compare_generated(_prefs, commandline)
+        # test a specific section
+        _prefs = {'browser.startup.homepage': 'http://github.com/'}
+        commandline[-1] = commandline[-1] + ':foo'
+        self.compare_generated(_prefs, commandline)
 
-        finally:
-            # cleanup
-            os.remove(name)
+        # cleanup
+        os.remove(name)
 
     def test_reset_should_remove_added_prefs(self):
         """Check that when we call reset the items we expect are updated"""
 
         profile = Profile()
         prefs_file = os.path.join(profile.profile, 'user.js')
 
         # we shouldn't have any initial preferences
@@ -116,18 +104,18 @@ browser.startup.homepage = http://github
         self.assertEqual(prefs1, Preferences.read_prefs(prefs_file))
         lines = file(prefs_file).read().strip().splitlines()
         self.assertTrue(bool([line for line in lines
                               if line.startswith('#MozRunner Prefs Start')]))
         self.assertTrue(bool([line for line in lines
                               if line.startswith('#MozRunner Prefs End')]))
 
         profile.reset()
-        self.assertNotEqual(prefs1,
-                            Preferences.read_prefs(os.path.join(profile.profile, 'user.js')),
+        self.assertNotEqual(prefs1, \
+                    Preferences.read_prefs(os.path.join(profile.profile, 'user.js')),\
                             "I pity the fool who left my pref")
 
     def test_magic_markers(self):
         """ensure our magic markers are working"""
 
         profile = Profile()
         prefs_file = os.path.join(profile.profile, 'user.js')
 
@@ -199,60 +187,27 @@ user_pref("webgl.force-enabled", true);
 
             # clean up the added preferences
             profile.cleanup()
             del profile
 
             # make sure you have the original preferences
             prefs = Preferences.read_prefs(user_js)
             self.assertTrue(prefs == original_prefs)
-        finally:
+        except:
             shutil.rmtree(tempdir)
+            raise
 
     def test_json(self):
         _prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"}
         json = '{"browser.startup.homepage": "http://planet.mozilla.org/"}'
 
         # just repr it...could use the json module but we don't need it here
         fd, name = tempfile.mkstemp(suffix='.json')
         os.write(fd, json)
         os.close(fd)
 
-        commandline = ["--preferences", name]
+        commandline = ["mozprofile", "--preferences", name]
         self.compare_generated(_prefs, commandline)
 
-    def test_prefs_write(self):
-        """test that the Preferences.write() method correctly serializes preferences"""
-
-        _prefs = {'browser.startup.homepage': "http://planet.mozilla.org",
-                  'zoom.minPercent': 30,
-                  'zoom.maxPercent': 300}
-
-        # make a Preferences manager with the testing preferences
-        preferences = Preferences(_prefs)
-
-        # write them to a temporary location
-        path = None
-        try:
-            with tempfile.NamedTemporaryFile(suffix='.js', delete=False) as f:
-                path = f.name
-                preferences.write(f, _prefs)
-
-            # read them back and ensure we get what we put in
-            self.assertEqual(dict(Preferences.read_prefs(path)), _prefs)
-
-        finally:
-            # cleanup
-            os.remove(path)
-
-    def test_read_prefs_with_comments(self):
-        """test reading preferences from a prefs.js file that contains comments"""
-
-        _prefs = {'browser.startup.homepage': 'http://planet.mozilla.org',
-                  'zoom.minPercent': 30,
-                  'zoom.maxPercent': 300,
-                  'webgl.verbose': 'false'}
-        path = os.path.join(here, 'files', 'prefs_with_comments.js')
-        self.assertEqual(dict(Preferences.read_prefs(path)), _prefs)
-
 
 if __name__ == '__main__':
     unittest.main()
deleted file mode 100755
--- a/testing/mozbase/mozprofile/tests/test_webapps.py
+++ /dev/null
@@ -1,197 +0,0 @@
-#!/usr/bin/env python
-
-"""
-test installing and managing webapps in a profile
-"""
-
-import os
-import shutil
-import unittest
-from tempfile import mkdtemp
-
-from mozprofile.webapps import WebappCollection, Webapp, WebappFormatException
-
-here = os.path.dirname(os.path.abspath(__file__))
-
-class WebappTest(unittest.TestCase):
-    """Tests reading, installing and cleaning webapps
-    from a profile.
-    """
-    manifest_path_1 = os.path.join(here, 'files', 'webapps1.json')
-    manifest_path_2 = os.path.join(here, 'files', 'webapps2.json')
-
-    def setUp(self):
-        self.profile = mkdtemp(prefix='test_webapp')
-        self.webapps_dir = os.path.join(self.profile, 'webapps')
-        self.webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json')
-
-    def tearDown(self):
-        shutil.rmtree(self.profile)
-
-    def test_read_json_manifest(self):
-        """Tests WebappCollection.read_json"""
-        # Parse a list of webapp objects and verify it worked
-        manifest_json_1 = WebappCollection.read_json(self.manifest_path_1)
-        self.assertEqual(len(manifest_json_1), 7)
-        for app in manifest_json_1:
-            self.assertIsInstance(app, Webapp)
-            for key in Webapp.required_keys:
-                self.assertIn(key, app)
-
-        # Parse a dictionary of webapp objects and verify it worked
-        manifest_json_2 = WebappCollection.read_json(self.manifest_path_2)
-        self.assertEqual(len(manifest_json_2), 5)
-        for app in manifest_json_2:
-            self.assertIsInstance(app, Webapp)
-            for key in Webapp.required_keys:
-                self.assertIn(key, app)
-
-    def test_invalid_webapp(self):
-        """Tests a webapp with a missing required key"""
-        webapps = WebappCollection(self.profile)
-        # Missing the required key "description", exception should be raised
-        self.assertRaises(WebappFormatException, webapps.append, { 'name': 'foo' })
-
-    def test_webapp_collection(self):
-        """Tests the methods of the WebappCollection object"""
-        webapp_1 = { 'name': 'test_app_1',
-                     'description': 'a description',
-                     'manifestURL': 'http://example.com/1/manifest.webapp',
-                     'appStatus': 1 }
-
-        webapp_2 = { 'name': 'test_app_2',
-                     'description': 'another description',
-                     'manifestURL': 'http://example.com/2/manifest.webapp',
-                     'appStatus': 2 }
-
-        webapp_3 = { 'name': 'test_app_2',
-                     'description': 'a third description',
-                     'manifestURL': 'http://example.com/3/manifest.webapp',
-                     'appStatus': 3 }
-
-        webapps = WebappCollection(self.profile)
-        self.assertEqual(len(webapps), 0)
-
-        # WebappCollection should behave like a list
-        def invalid_index():
-            webapps[0]
-        self.assertRaises(IndexError, invalid_index)
-
-        # Append a webapp object
-        webapps.append(webapp_1)
-        self.assertTrue(len(webapps), 1)
-        self.assertIsInstance(webapps[0], Webapp)
-        self.assertEqual(len(webapps[0]), len(webapp_1))
-        self.assertEqual(len(set(webapps[0].items()) & set(webapp_1.items())), len(webapp_1))
-
-        # Remove a webapp object
-        webapps.remove(webapp_1)
-        self.assertEqual(len(webapps), 0)
-
-        # Extend a list of webapp objects
-        webapps.extend([webapp_1, webapp_2])
-        self.assertEqual(len(webapps), 2)
-        self.assertTrue(webapp_1 in webapps)
-        self.assertTrue(webapp_2 in webapps)
-        self.assertNotEquals(webapps[0], webapps[1])
-
-        # Insert a webapp object
-        webapps.insert(1, webapp_3)
-        self.assertEqual(len(webapps), 3)
-        self.assertEqual(webapps[1], webapps[2])
-        for app in webapps:
-            self.assertIsInstance(app, Webapp)
-
-        # Assigning an invalid type (must be accepted by the dict() constructor) should throw
-        def invalid_type():
-            webapps[2] = 1
-        self.assertRaises(WebappFormatException, invalid_type)
-
-    def test_install_webapps(self):
-        """Test installing webapps into a profile that has no prior webapps"""
-        webapps = WebappCollection(self.profile, apps=self.manifest_path_1)
-        self.assertFalse(os.path.exists(self.webapps_dir))
-
-        # update the webapp manifests for the first time
-        webapps.update_manifests()
-        self.assertFalse(os.path.isdir(os.path.join(self.profile, webapps.backup_dir)))
-        self.assertTrue(os.path.isfile(self.webapps_json_path))
-
-        webapps_json = webapps.read_json(self.webapps_json_path, description="fake description")
-        self.assertEqual(len(webapps_json), 7)
-        for app in webapps_json:
-            self.assertIsInstance(app, Webapp)
-
-        manifest_json_1 = webapps.read_json(self.manifest_path_1)
-        manifest_json_2 = webapps.read_json(self.manifest_path_2)
-        self.assertEqual(len(webapps_json), len(manifest_json_1))
-        for app in webapps_json:
-            self.assertTrue(app in manifest_json_1)
-
-        # Remove one of the webapps from WebappCollection after it got installed
-        removed_app = manifest_json_1[2]
-        webapps.remove(removed_app)
-        # Add new webapps to the collection
-        webapps.extend(manifest_json_2)
-
-        # update the webapp manifests a second time
-        webapps.update_manifests()
-        self.assertFalse(os.path.isdir(os.path.join(self.profile, webapps.backup_dir)))
-        self.assertTrue(os.path.isfile(self.webapps_json_path))
-
-        webapps_json = webapps.read_json(self.webapps_json_path, description="a description")
-        self.assertEqual(len(webapps_json), 11)
-
-        # The new apps should be added
-        for app in webapps_json:
-            self.assertIsInstance(app, Webapp)
-            self.assertTrue(os.path.isfile(os.path.join(self.webapps_dir, app['name'], 'manifest.webapp')))
-        # The removed app should not exist in the manifest
-        self.assertNotIn(removed_app, webapps_json)
-        self.assertFalse(os.path.exists(os.path.join(self.webapps_dir, removed_app['name'])))
-
-        # Cleaning should delete the webapps directory entirely since there was nothing there before
-        webapps.clean()
-        self.assertFalse(os.path.isdir(self.webapps_dir))
-
-    def test_install_webapps_preexisting(self):
-        """Tests installing webapps when the webapps directory already exists"""
-        manifest_json_2 = WebappCollection.read_json(self.manifest_path_2)
-
-        # Synthesize a pre-existing webapps directory
-        os.mkdir(self.webapps_dir)
-        shutil.copyfile(self.manifest_path_2, self.webapps_json_path)
-        for app in manifest_json_2:
-            app_path = os.path.join(self.webapps_dir, app['name'])
-            os.mkdir(app_path)
-            f = open(os.path.join(app_path, 'manifest.webapp'), 'w')
-            f.close()
-
-        webapps = WebappCollection(self.profile, apps=self.manifest_path_1)
-        self.assertTrue(os.path.exists(self.webapps_dir))
-
-        # update webapp manifests for the first time
-        webapps.update_manifests()
-        # A backup should be created
-        self.assertTrue(os.path.isdir(os.path.join(self.profile, webapps.backup_dir)))
-
-        # Both manifests should remain installed
-        webapps_json = webapps.read_json(self.webapps_json_path, description='a fake description')
-        self.assertEqual(len(webapps_json), 12)
-        for app in webapps_json:
-            self.assertIsInstance(app, Webapp)
-            self.assertTrue(os.path.isfile(os.path.join(self.webapps_dir, app['name'], 'manifest.webapp')))
-
-        # Upon cleaning the backup should be restored
-        webapps.clean()
-        self.assertFalse(os.path.isdir(os.path.join(self.profile, webapps.backup_dir)))
-
-        # The original webapps should still be installed
-        webapps_json = webapps.read_json(self.webapps_json_path)
-        for app in webapps_json:
-            self.assertIsInstance(app, Webapp)
-            self.assertTrue(os.path.isfile(os.path.join(self.webapps_dir, app['name'], 'manifest.webapp')))
-        self.assertEqual(webapps_json, manifest_json_2)
-
-if __name__ == '__main__':
-    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/README.md
@@ -0,0 +1,43 @@
+[mozrunner](https://github.com/mozilla/mozbase/tree/master/mozrunner)
+is a [python package](http://pypi.python.org/pypi/mozrunner)
+which handles running of Mozilla applications.
+mozrunner utilizes [mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile)
+for managing application profiles
+and [mozprocess](https://github.com/mozilla/mozbase/tree/master/mozprocess) for robust process control.
+
+mozrunner may be used from the command line or programmatically as an API.
+
+
+# Command Line Usage
+
+The `mozrunner` command will launch the application (specified by
+`--app`) from a binary specified with `-b` or as located on the `PATH`.
+
+mozrunner takes the command line options from 
+[mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile) for constructing the profile to be used by 
+the application.
+
+Run `mozrunner --help` for detailed information on the command line
+program.
+
+
+# API Usage
+
+mozrunner features a base class, 
+[mozrunner.runner.Runner](https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py) 
+which is an integration layer API for interfacing with Mozilla applications.
+
+mozrunner also exposes two application specific classes,
+`FirefoxRunner` and `ThunderbirdRunner` which record the binary names
+necessary for the `Runner` class to find them on the system.
+
+Example API usage:
+
+    from mozrunner import FirefoxRunner
+	
+    # start Firefox on a new profile
+    runner = FirefoxRunner()
+    runner.start()
+
+See also a comparable implementation for [selenium](http://seleniumhq.org/): 
+http://code.google.com/p/selenium/source/browse/trunk/py/selenium/webdriver/firefox/firefox_binary.py
\ No newline at end of file
--- a/testing/mozbase/mozrunner/setup.py
+++ b/testing/mozbase/mozrunner/setup.py
@@ -1,44 +1,51 @@
 # 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 os
 import sys
 from setuptools import setup
 
 PACKAGE_NAME = "mozrunner"
-PACKAGE_VERSION = '5.15'
+PACKAGE_VERSION = '5.14'
 
 desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
+# take description from README
+here = os.path.dirname(os.path.abspath(__file__))
+try:
+    description = file(os.path.join(here, 'README.md')).read()
+except (OSError, IOError):
+    description = ''
 
-deps = ['mozinfo >= 0.4',
-        'mozprocess >= 0.8',
-        'mozprofile >= 0.4',
+deps = ['mozinfo == 0.4',
+        'mozprocess == 0.8',
+        'mozprofile == 0.4',
        ]
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
       description=desc,
-      long_description="see http://mozbase.readthedocs.org/",
+      long_description=description,
       classifiers=['Environment :: Console',
                    'Intended Audience :: Developers',
                    'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
                    'Natural Language :: English',
                    'Operating System :: OS Independent',
                    'Programming Language :: Python',
                    'Topic :: Software Development :: Libraries :: Python Modules',
                    ],
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
       license='MPL 2.0',
       packages=['mozrunner'],
       zip_safe=False,
       install_requires = deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       mozrunner = mozrunner:cli