Bug 795496 - Make mozdevice raise exceptions on error;r=ahal,jmaher
authorWilliam Lachance <wlachance@mozilla.com>
Thu, 04 Oct 2012 11:28:07 -0400
changeset 112851 6e34c2118a9254b02e92c6c1dabb26aaef46d3f5
parent 112850 267d712aabddadee97b7d883a842c6c9a9bd271b
child 112852 d30e007d711f671923702c6b1c8c1765a15e1da0
push idunknown
push userunknown
push dateunknown
reviewersahal, jmaher
bugs795496
milestone18.0a1
Bug 795496 - Make mozdevice raise exceptions on error;r=ahal,jmaher It turns out that relying on the user to check return codes for every command was non-intuitive and resulted in many hard to trace bugs. Now most functinos just return "None", and raise a DMError when there's an exception. The exception to this are functions like dirExists, which now return booleans, and throw exceptions on error. This is a fairly major refactor, and also involved the following internal changes: * Removed FileError and AgentError exceptions, replaced with DMError (having to manage three different types of exceptions was confusing, all the more so when we're raising them) * Docstrings updated to remove references to return values where no longer relevant * pushFile no longer will create a directory to accomodate the file if it doesn't exist (this makes it consistent with devicemanagerADB) * dmSUT we validate the file, but assume that we get something back from the agent, instead of falling back to manual validation in the case that we didn't * isDir and dirExists had the same intention, but different implementations for dmSUT. Replaced the dmSUT impl of getDirectory with that of isDir's (which was much simpler). Removed isDir from devicemanager.py, since it wasn't used externally * killProcess modified to check for process existence before running (since the actual internal kill command will throw an exception if the process doesn't exist) In addition to all this, more unit tests have been added to test these changes for devicemanagerSUT.
build/mobile/remoteautomation.py
layout/tools/reftest/remotereftest.py
layout/tools/reftest/runreftestb2g.py
testing/mochitest/runtestsb2g.py
testing/mochitest/runtestsremote.py
testing/mozbase/mozdevice/mozdevice/devicemanager.py
testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
testing/mozbase/mozdevice/mozdevice/sutcli.py
testing/mozbase/mozdevice/tests/manifest.ini
testing/mozbase/mozdevice/tests/sut.py
--- a/build/mobile/remoteautomation.py
+++ b/build/mobile/remoteautomation.py
@@ -5,17 +5,17 @@
 import time
 import os
 import automationutils
 import tempfile
 import shutil
 import subprocess
 
 from automation import Automation
-from devicemanager import NetworkTools
+from devicemanager import NetworkTools, DMError
 
 class RemoteAutomation(Automation):
     _devicemanager = None
 
     def __init__(self, deviceManager, appName = '', remoteLog = None):
         self._devicemanager = deviceManager
         self._appName = appName
         self._remoteProfile = None
@@ -152,29 +152,41 @@ class RemoteAutomation(Automation):
 
             # Setting timeout at 1 hour since on a remote device this takes much longer
             self.timeout = 3600
             # The benefit of the following sleep is unclear; it was formerly 15 seconds
             time.sleep(1)
 
         @property
         def pid(self):
-            hexpid = self.dm.processExist(self.procName)
-            if (hexpid == None):
-                hexpid = "0x0"
-            return int(hexpid, 0)
+            pid = self.dm.processExist(self.procName)
+            # HACK: we should probably be more sophisticated about monitoring
+            # running processes for the remote case, but for now we'll assume
+            # that this method can be called when nothing exists and it is not
+            # an error
+            if pid is None:
+                return 0
+            return pid
 
         @property
         def stdout(self):
-            t = self.dm.getFile(self.proc)
-            if t == None: return ''
-            tlen = len(t)
-            retVal = t[self.stdoutlen:]
-            self.stdoutlen = tlen
-            return retVal.strip('\n').strip()
+            if self.dm.fileExists(self.proc):
+                try:
+                    t = self.dm.pullFile(self.proc)
+                except DMError:
+                    # we currently don't retry properly in the pullFile
+                    # function in dmSUT, so an error here is not necessarily
+                    # the end of the world
+                    return ''
+                tlen = len(t)
+                retVal = t[self.stdoutlen:]
+                self.stdoutlen = tlen
+                return retVal.strip('\n').strip()
+            else:
+                return ''
 
         def wait(self, timeout = None):
             timer = 0
             interval = 5
 
             if timeout == None:
                 timeout = self.timeout
 
--- a/layout/tools/reftest/remotereftest.py
+++ b/layout/tools/reftest/remotereftest.py
@@ -323,23 +323,29 @@ user_pref("reftest.uri", "%s");
             fhandle.write("""
 user_pref("capability.principal.codebase.p2.granted", "UniversalXPConnect");
 user_pref("capability.principal.codebase.p2.id", "http://%s:%s");
 """ % (options.remoteWebServer, options.httpPort))
 
         # Close the file
         fhandle.close()
 
-        if (self._devicemanager.pushDir(profileDir, options.remoteProfile) == None):
-            raise devicemanager.FileError("Failed to copy profiledir to device")
+        try:
+            self._devicemanager.pushDir(profileDir, options.remoteProfile)
+        except devicemanager.DMError:
+            print "Automation Error: Failed to copy profiledir to device"
+            raise
 
     def copyExtraFilesToProfile(self, options, profileDir):
         RefTest.copyExtraFilesToProfile(self, options, profileDir)
-        if (self._devicemanager.pushDir(profileDir, options.remoteProfile) == None):
-            raise devicemanager.FileError("Failed to copy extra files to device")
+        try:
+            self._devicemanager.pushDir(profileDir, options.remoteProfile)
+        except devicemanager.DMError:
+            print "Automation Error: Failed to copy extra files to device"
+            raise
 
     def getManifestPath(self, path):
         return path
 
     def cleanup(self, profileDir):
         # Pull results back from device
         if (self.remoteLogFile):
             try:
--- a/layout/tools/reftest/runreftestb2g.py
+++ b/layout/tools/reftest/runreftestb2g.py
@@ -19,16 +19,17 @@ from automation import Automation
 from b2gautomation import B2GRemoteAutomation
 from runreftest import RefTest
 from runreftest import ReftestOptions
 from remotereftest import ReftestServer
 
 from mozprofile import Profile
 from mozrunner import Runner
 
+import devicemanager
 import devicemanagerADB
 import manifestparser
 
 from marionette import Marionette
 
 
 class B2GOptions(ReftestOptions):
 
@@ -325,17 +326,17 @@ class B2GReftest(RefTest):
     def stopWebServer(self, options):
         if hasattr(self, 'server'):
             self.server.stop()
 
 
     def restoreProfilesIni(self):
         # restore profiles.ini on the device to its previous state
         if not self.originalProfilesIni or not os.access(self.originalProfilesIni, os.F_OK):
-            raise DMError('Unable to install original profiles.ini; file not found: %s',
+            raise devicemanager.DMError('Unable to install original profiles.ini; file not found: %s',
                           self.originalProfilesIni)
 
         self._devicemanager.pushFile(self.originalProfilesIni, self.remoteProfilesIniPath)
 
     def updateProfilesIni(self, profilePath):
         # update profiles.ini on the device to point to the test profile
         self.originalProfilesIni = tempfile.mktemp()
         self._devicemanager.getFile(self.remoteProfilesIniPath, self.originalProfilesIni)
@@ -389,18 +390,21 @@ user_pref("capability.principal.codebase
 user_pref("capability.principal.codebase.p2.id", "http://%s:%s");
 """ % (options.remoteWebServer, options.httpPort))
 
         # Close the file
         fhandle.close()
 
         # Copy the profile to the device.
         self._devicemanager.removeDir(self.remoteProfile)
-        if self._devicemanager.pushDir(profileDir, self.remoteProfile) == None:
-            raise devicemanager.FileError("Unable to copy profile to device.")
+        try:
+            self._devicemanager.pushDir(profileDir, self.remoteProfile)
+        except devicemanager.DMError:
+            print "Automation Error: Unable to copy profile to device."
+            raise
 
         # In B2G, user.js is always read from /data/local, not the profile
         # directory.  Backup the original user.js first so we can restore it.
         self._devicemanager.checkCmdAs(['shell', 'rm', '-f', '%s.orig' % self.userJS])
         if self._devicemanager.useDDCopy:
             self._devicemanager.checkCmdAs(['shell', 'dd', 'if=%s' % self.userJS, 'of=%s.orig' % self.userJS])
         else:
             self._devicemanager.checkCmdAs(['shell', 'cp', self.userJS, '%s.orig' % self.userJS])
@@ -408,18 +412,21 @@ user_pref("capability.principal.codebase
 
         self.updateProfilesIni(self.remoteProfile)
 
         options.profilePath = self.remoteProfile
         return retVal
 
     def copyExtraFilesToProfile(self, options, profileDir):
         RefTest.copyExtraFilesToProfile(self, options, profileDir)
-        if (self._devicemanager.pushDir(profileDir, options.remoteProfile) == None):
-            raise devicemanager.FileError("Failed to copy extra files to device")
+        try:
+            self._devicemanager.pushDir(profileDir, options.remoteProfile)
+        except devicemanager.DMError:
+            print "Automation Error: Failed to copy extra files to device"
+            raise
 
     def getManifestPath(self, path):
         return path
 
 
 def main(args=sys.argv[1:]):
     auto = B2GRemoteAutomation(None, "fennec", context_chrome=True)
     parser = B2GOptions(auto)
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -14,16 +14,17 @@ import traceback
 sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))))
 
 from automation import Automation
 from b2gautomation import B2GRemoteAutomation
 from runtests import Mochitest
 from runtests import MochitestOptions
 from runtests import MochitestServer
 
+import devicemanager
 import devicemanagerADB
 import manifestparser
 
 from marionette import Marionette
 
 
 class B2GOptions(MochitestOptions):
 
@@ -383,18 +384,21 @@ user_pref("dom.ipc.browser_frames.oop_by
 user_pref("browser.manifestURL","app://system.gaiamobile.org/manifest.webapp");\n
 user_pref("dom.mozBrowserFramesWhitelist","app://system.gaiamobile.org,http://mochi.test:8888");\n
 user_pref("network.dns.localDomains","app://system.gaiamobile.org");\n
 """)
         f.close()
 
         # Copy the profile to the device.
         self._dm._checkCmdAs(['shell', 'rm', '-r', self.remoteProfile])
-        if self._dm.pushDir(options.profilePath, self.remoteProfile) == None:
-            raise devicemanager.FileError("Unable to copy profile to device.")
+        try:
+            self._dm.pushDir(options.profilePath, self.remoteProfile)
+        except devicemanager.DMError:
+            print "Automation Error: Unable to copy profile to device."
+            raise
 
         # In B2G, user.js is always read from /data/local, not the profile
         # directory.  Backup the original user.js first so we can restore it.
         if not self._dm.fileExists('%s.orig' % self.userJS):
             self.copyRemoteFile(self.userJS, '%s.orig' % self.userJS)
         self._dm.pushFile(os.path.join(options.profilePath, "user.js"), self.userJS)
         self.updateProfilesIni(self.remoteProfile)
         options.profilePath = self.remoteProfile
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -279,43 +279,53 @@ class MochiRemote(Mochitest):
         self.server.stop()
         
     def buildProfile(self, options):
         if self.localProfile:
             options.profilePath = self.localProfile
         manifest = Mochitest.buildProfile(self, options)
         self.localProfile = options.profilePath
         self._dm.removeDir(self.remoteProfile)
-        if self._dm.pushDir(options.profilePath, self.remoteProfile) == None:
-            raise devicemanager.FileError("Unable to copy profile to device.")
+        try:
+            self._dm.pushDir(options.profilePath, self.remoteProfile)
+        except devicemanager.DMError:
+            print "Automation Error: Unable to copy profile to device."
+            raise
 
         options.profilePath = self.remoteProfile
         return manifest
     
     def buildURLOptions(self, options, env):
         self.localLog = options.logFile
         options.logFile = self.remoteLog
         options.profilePath = self.localProfile
         retVal = Mochitest.buildURLOptions(self, options, env)
         #we really need testConfig.js (for browser chrome)
-        if self._dm.pushDir(options.profilePath, self.remoteProfile) == None:
-            raise devicemanager.FileError("Unable to copy profile to device.")
+        try:
+            self._dm.pushDir(options.profilePath, self.remoteProfile)
+        except devicemanager.DMError:
+            print "Automation Error: Unable to copy profile to device."
+            raise
 
         options.profilePath = self.remoteProfile
         options.logFile = self.localLog
         return retVal
 
     def installChromeFile(self, filename, options):
         parts = options.app.split('/')
         if (parts[0] == options.app):
           return "NO_CHROME_ON_DROID"
         path = '/'.join(parts[:-1])
         manifest = path + "/chrome/" + os.path.basename(filename)
-        if self._dm.pushFile(filename, manifest) == False:
-            raise devicemanager.FileError("Unable to install Chrome files on device.")
+        try:
+            self._dm.pushFile(filename, manifest)
+        except devicemanager.DMError:
+            print "Automation Error: Unable to install Chrome files on device."
+            raise
+
         return manifest
 
     def getLogFilePath(self, logFile):             
         return logFile
 
     # In the future we could use LogParser: http://hg.mozilla.org/automation/logparser/
     def addLogData(self):
         with open(self.localLog) as currentLog:
--- a/testing/mozbase/mozdevice/mozdevice/devicemanager.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
@@ -3,30 +3,22 @@
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import hashlib
 import socket
 import os
 import re
 import StringIO
 
-class FileError(Exception):
-    " Signifies an error which occurs while doing a file operation."
-
-    def __init__(self, msg = ''):
-        self.msg = msg
-
-    def __str__(self):
-        return self.msg
-
 class DMError(Exception):
     "generic devicemanager exception."
 
-    def __init__(self, msg= ''):
+    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
     def not_implemented(*args, **kwargs):
@@ -35,119 +27,79 @@ def abstractmethod(method):
                                    (repr(method), filename, line))
     return not_implemented
 
 class DeviceManager:
 
     @abstractmethod
     def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
         """
-        Executes shell command on device.
+        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
-
-        returns:
-          success: Return code from command
-          failure: None
         """
 
     def shellCheckOutput(self, cmd, env=None, cwd=None, timeout=None, root=False):
         """
-        executes shell command on device (with root privileges if
-        specified)  and returns the the output
+        executes shell command on device and returns the the output
 
-        timeout is specified in seconds, and if no timeout is given,
-        we will run until the script returns
-        returns:
-        success: Returns output of shell command
-        failure: DMError will be raised
+        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 is None:
-            raise DMError("Did not successfully run command %s (output: '%s', retval: 'None')" % (cmd, output))
         if retval != 0:
             raise DMError("Non-zero return code for command: %s (output: '%s', retval: '%i')" % (cmd, output, retval))
         return output
 
     @abstractmethod
     def pushFile(self, localname, destname):
         """
         Copies localname from the host to destname on the device
-
-        returns:
-          success: True
-          failure: False
         """
 
     @abstractmethod
     def mkDir(self, name):
         """
         Creates a single directory on the device file system
-
-        returns:
-          success: directory name
-          failure: None
         """
 
     def mkDirs(self, filename):
         """
         Make directory structure on the device
         WARNING: does not create last part of the path
-
-        returns:
-          success: directory structure that we created
-          failure: None
         """
         parts = filename.split('/')
         name = ""
         for part in parts:
             if (part == parts[-1]):
                 break
             if (part != ""):
                 name += '/' + part
-                if (not self.dirExists(name)):
-                    if (self.mkDir(name) == None):
-                        print "Automation Error: failed making directory: " + str(name)
-                        return None
-        return name
+                self.mkDir(name) # mkDir will check previous existence
 
     @abstractmethod
     def pushDir(self, localDir, remoteDir):
         """
         Push localDir from host to remoteDir on the device
-
-        returns:
-          success: remoteDir
-          failure: None
-        """
-
-    @abstractmethod
-    def dirExists(self, dirname):
-        """
-        Checks if dirname exists and is a directory
-        on the device file system
-
-        returns:
-          success: True
-          failure: False
         """
 
     @abstractmethod
     def fileExists(self, filepath):
         """
-        Checks if filepath exists and is a file on
-        the device file system
+        Checks if filepath exists and is a file on the device file system
 
         returns:
           success: True
           failure: False
         """
 
     @abstractmethod
     def listFiles(self, rootdir):
@@ -481,72 +433,54 @@ class DeviceManager:
           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:
-          success: dict of info strings by directive name
-          failure: None
+        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)
-
-        returns:
-          success: None
-          failure: error string
         """
 
     @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)
-
-        returns:
-          success: None
-          failure: DMError exception thrown
         """
 
     @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)
-
-        returns:
-          success: None
-          failure: DMError exception thrown
         """
 
     @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
-
-        returns:
-          success: text status from command or callback server
-          failure: None
         """
 
     @abstractmethod
     def getCurrentTime(self):
         """
         Returns device time in milliseconds since the epoch
 
         returns:
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
@@ -76,28 +76,24 @@ class DeviceManagerADB(DeviceManager):
             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.
+        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
-
-        returns:
-          success: Return code from command
-          failure: None
         """
         # 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 "
@@ -158,73 +154,58 @@ class DeviceManagerADB(DeviceManager):
         self._checkCmd(["connect", self.host + ":" + str(self.port)])
 
     def _disconnectRemoteADB(self):
         self._checkCmd(["disconnect", self.host + ":" + str(self.port)])
 
     def pushFile(self, localname, destname):
         """
         Copies localname from the host to destname on the device
-
-        returns:
-          success: True
-          failure: False
         """
         try:
             if (os.name == "nt"):
                 destname = destname.replace('\\', '/')
             if (self.useRunAs):
                 remoteTmpFile = self.getTempDir() + "/" + os.path.basename(localname)
                 self._checkCmd(["push", os.path.realpath(localname), remoteTmpFile])
                 if self.useDDCopy:
                     self._checkCmdAs(["shell", "dd", "if=" + remoteTmpFile, "of=" + destname])
                 else:
                     self._checkCmdAs(["shell", "cp", remoteTmpFile, destname])
                 self._checkCmd(["shell", "rm", remoteTmpFile])
             else:
                 self._checkCmd(["push", os.path.realpath(localname), destname])
-            if (self.isDir(destname)):
+            if (self.dirExists(destname)):
                 destname = destname + "/" + os.path.basename(localname)
             return True
         except:
-            return False
+            raise DMError("Error pushing file to device")
 
     def mkDir(self, name):
         """
         Creates a single directory on the device file system
-
-        returns:
-          success: directory name
-          failure: None
         """
         try:
             result = self._runCmdAs(["shell", "mkdir", name]).stdout.read()
             if 'read-only file system' in result.lower():
-                return None
-            if 'file exists' in result.lower():
-                return name
-            return name
+                raise DMError("Error creating directory: read only file system")
+            # otherwise assume success
         except:
-            return None
+            raise DMError("Error creating directory")
 
     def pushDir(self, localDir, remoteDir):
         """
         Push localDir from host to remoteDir on the device
-
-        returns:
-          success: remoteDir
-          failure: None
         """
         # 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 push file-by-file to get around this
         # limitation
-        try:
-            if (not self.dirExists(remoteDir)):
-                self.mkDirs(remoteDir+"/x")
+        if (not self.dirExists(remoteDir)):
+            self.mkDirs(remoteDir+"/x")
             if (self.useZip):
                 try:
                     localZip = tempfile.mktemp()+".zip"
                     remoteZip = remoteDir + "/adbdmtmp.zip"
                     subprocess.check_output(["zip", "-r", localZip, '.'], cwd=localDir)
                     self.pushFile(localZip, remoteZip)
                     os.remove(localZip)
                     data = self._runCmdAs(["shell", "unzip", "-o", remoteZip, "-d", remoteDir]).stdout.read()
@@ -247,115 +228,75 @@ class DeviceManagerADB(DeviceManager):
                         self.pushFile(localFile, remoteFile)
                     for d in dirs:
                         targetDir = remoteDir + "/"
                         if (relRoot!="."):
                             targetDir = targetDir + relRoot + "/"
                         targetDir = targetDir + d
                         if (not self.dirExists(targetDir)):
                             self.mkDir(targetDir)
-            return remoteDir
-        except:
-            print "pushing " + localDir + " to " + remoteDir + " failed"
-            return None
 
-    def dirExists(self, dirname):
+    def dirExists(self, remotePath):
+        """
+        Return True if remotePath is an existing directory on the device.
         """
-        Checks if dirname exists and is a directory
-        on the device file system
+        p = self._runCmd(["shell", "ls", "-a", remotePath + '/'])
 
-        returns:
-          success: True
-          failure: False
-        """
-        return self.isDir(dirname)
+        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
 
-    # Because we always have / style paths we make this a lot easier with some
-    # assumptions
     def fileExists(self, filepath):
         """
-        Checks if filepath exists and is a file on
-        the device file system
-
-        returns:
-          success: True
-          failure: False
+        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
-
-        returns:
-          success: output of telnet
-          failure: None
         """
-        return self._runCmd(["shell", "rm", filename]).stdout.read()
+        if self.fileExists(filename):
+            self._runCmd(["shell", "rm", filename])
 
     def _removeSingleDir(self, remoteDir):
         """
         Deletes a single empty directory
-
-        returns:
-          success: output of telnet
-          failure: None
         """
         return self._runCmd(["shell", "rmdir", remoteDir]).stdout.read()
 
     def removeDir(self, remoteDir):
         """
         Does a recursive delete of directory on the device: rm -Rf remoteDir
-
-        returns:
-          success: output of telnet
-          failure: None
         """
-        out = ""
-        if (self.isDir(remoteDir)):
+        if (self.dirExists(remoteDir)):
             files = self.listFiles(remoteDir.strip())
             for f in files:
-                if (self.isDir(remoteDir.strip() + "/" + f.strip())):
-                    out += self.removeDir(remoteDir.strip() + "/" + f.strip())
+                path = remoteDir.strip() + "/" + f.strip()
+                if self.dirExists(path):
+                    self.removeDir(path)
                 else:
-                    out += self.removeFile(remoteDir.strip() + "/" + f.strip())
-            out += self._removeSingleDir(remoteDir.strip())
+                    self.removeFile(path)
+            self._removeSingleDir(remoteDir.strip())
         else:
-            out += self.removeFile(remoteDir.strip())
-        return out
-
-    def isDir(self, remotePath):
-        """
-        Checks if remotePath is a directory on the device
-
-        returns:
-          success: True
-          failure: False
-        """
-        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
+            self.removeFile(remoteDir.strip())
 
     def listFiles(self, rootdir):
         """
-        Lists files on the device rootdir, requires cd to directory first
+        Lists files on the device rootdir
 
-        returns:
-          success: array of filenames, ['file1', 'file2', ...]
-          failure: None
+        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):
@@ -384,35 +325,36 @@ class DeviceManagerADB(DeviceManager):
         while (proc):
             els = proc.split()
             ret.append(list([els[1], els[len(els) - 1], els[0]]))
             proc =  p.stdout.readline()
         return ret
 
     def fireProcess(self, appname, failIfRunning=False):
         """
-        DEPRECATED: Use shell() or launchApplication() for new code
+        Starts a process
 
-        returns:
-          success: pid
-          failure: None
+        returns: pid
+
+        DEPRECATED: Use shell() or launchApplication() for new code
         """
         #strip out env vars
         parts = appname.split('"');
         if (len(parts) > 2):
             parts = parts[2:]
         return self.launchProcess(parts, failIfRunning)
 
     def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False):
         """
-        DEPRECATED: Use shell() or launchApplication() for new code
+        Launches a process, redirecting output to standard out
 
-        returns:
-          success: output filename
-          failure: None
+        WARNING: Does not work how you expect on Android! The application's
+        own output will be flushed elsewhere.
+
+        DEPRECATED: Use shell() or launchApplication() for new code
         """
         if cmd[0] == "am":
             self._checkCmd(["shell"] + cmd)
             return outputFile
 
         acmd = ["shell", "am", "start", "-W"]
         cmd = ' '.join(cmd).strip()
         i = cmd.find(" ")
@@ -445,55 +387,41 @@ class DeviceManagerADB(DeviceManager):
             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
-
-        external function
-        returns:
-          success: True
-          failure: False
         """
         procs = self.getProcessList()
-        didKillProcess = False
         for (pid, name, user) in procs:
             if name == appname:
                 args = ["shell", "kill"]
                 if forceKill:
                     args.append("-9")
                 args.append(pid)
                 p = self._runCmdAs(args)
                 p.communicate()
-                if p.returncode == 0:
-                    didKillProcess = True
-
-        return didKillProcess
+                if p.returncode != 0:
+                    raise DMError("Error killing process "
+                                  "'%s': %s" % (appname, p.stdout.read()))
 
     def catFile(self, remoteFile):
         """
         Returns the contents of remoteFile
-
-        returns:
-          success: filecontents, string
-          failure: None
         """
         return self.pullFile(remoteFile)
 
     def _runPull(self, remoteFile, localFile):
         """
         Pulls remoteFile from device to host
-
-        returns:
-          success: path to localFile
-          failure: None
         """
         try:
             # First attempt to pull file regularly
             outerr = self._runCmd(["pull",  remoteFile, localFile]).communicate()
 
             # Now check stderr for errors
             if outerr[1]:
                 errl = outerr[1].splitlines()
@@ -505,266 +433,187 @@ class DeviceManagerADB(DeviceManager):
                         # to copy the file to a world-readable location first before attempting
                         # to pull it again.
                         remoteTmpFile = self.getTempDir() + "/" + os.path.basename(remoteFile)
                         self._checkCmdAs(["shell", "dd", "if=" + remoteFile, "of=" + remoteTmpFile])
                         self._checkCmdAs(["shell", "chmod", "777", remoteTmpFile])
                         self._runCmd(["pull",  remoteTmpFile, localFile]).stdout.read()
                         # Clean up temporary file
                         self._checkCmdAs(["shell", "rm", remoteTmpFile])
-            return localFile
         except (OSError, ValueError):
-            return None
+            raise DMError("Error pulling remote file '%s' to '%s'" % (remoteFile, localFile))
 
     def pullFile(self, remoteFile):
         """
         Returns contents of remoteFile using the "pull" command.
-
-        returns:
-          success: output of pullfile, string
-          failure: None
         """
         # TODO: add debug flags and allow for printing stdout
         localFile = tempfile.mkstemp()[1]
-        localFile = self._runPull(remoteFile, localFile)
-
-        if localFile is None:
-            print 'Automation Error: failed to pull file %s!' % remoteFile
-            return None
+        self._runPull(remoteFile, localFile)
 
         f = open(localFile, 'r')
         ret = f.read()
         f.close()
         os.remove(localFile)
         return ret
 
     def getFile(self, remoteFile, localFile = 'temp.txt'):
         """
-        Copy file from device (remoteFile) to host (localFile)
-
-        returns:
-          success: contents of file, string
-          failure: None
+        Copy file from device (remoteFile) to host (localFile).
         """
-        try:
-            contents = self.pullFile(remoteFile)
-        except:
-            return None
-
-        if contents is None:
-            return None
+        contents = self.pullFile(remoteFile)
 
         fhandle = open(localFile, 'wb')
         fhandle.write(contents)
         fhandle.close()
-        return contents
-
 
     def getDirectory(self, remoteDir, localDir, checkDir=True):
         """
         Copy directory structure from device (remoteDir) to host (localDir)
-
-        returns:
-          success: list of files, string
-          failure: None
         """
-        # checkDir has no affect in devicemanagerADB
-        ret = []
-        p = self._runCmd(["pull", remoteDir, localDir])
-        p.stdout.readline()
-        line = p.stdout.readline()
-        while (line):
-            els = line.split()
-            f = els[len(els) - 1]
-            i = f.find(localDir)
-            if (i != -1):
-                if (localDir[len(localDir) - 1] != '/'):
-                    i = i + 1
-                f = f[i + len(localDir):]
-            i = f.find("/")
-            if (i > 0):
-                f = f[0:i]
-            ret.append(f)
-            line =  p.stdout.readline()
-        #the last line is a summary
-        if (len(ret) > 0):
-            ret.pop()
-        return ret
-
-
+        self._runCmd(["pull", remoteDir, localDir])
 
     def validateFile(self, remoteFile, localFile):
         """
-        Checks if the remoteFile has the same md5 hash as the localFile
-
-        returns:
-          success: True/False
-          failure: None
+        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):
         """
         Return the md5 sum of a file on the device
-
-        returns:
-          success: MD5 hash for given filename
-          failure: None
         """
         localFile = tempfile.mkstemp()[1]
         localFile = self._runPull(remoteFile, localFile)
 
         if localFile is None:
             return None
 
         md5 = self._getLocalHash(localFile)
         os.remove(localFile)
 
         return md5
 
-    # setup the device root and cache its value
     def _setupDeviceRoot(self):
+        """
+        setup the device root and cache its value
+        """
         # if self.deviceRoot is already set, create it if necessary, and use it
         if self.deviceRoot:
             if not self.dirExists(self.deviceRoot):
                 if not self.mkDir(self.deviceRoot):
                     raise DMError("Unable to create device root %s" % self.deviceRoot)
             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
 
         for (basePath, subPath) in [('/mnt/sdcard', 'tests'),
-                                                                ('/data/local', 'tests')]:
+                                    ('/data/local', 'tests')]:
             if self.dirExists(basePath):
                 testRoot = os.path.join(basePath, subPath)
                 if self.mkDir(testRoot):
                     self.deviceRoot = testRoot
                     return
 
         raise DMError("Unable to set up device root as /mnt/sdcard/tests "
                                     "or /data/local/tests")
 
     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
         """
         return self.deviceRoot
 
     def getTempDir(self):
         """
-        Gets the temporary directory we are using on this device
-        base on our device root, ensuring also that it exists.
+        Return a temporary directory on the device
 
-        returns:
-          success: path for temporary directory
-          failure: None
+        Will also ensure that directory exists
         """
         # Cache result to speed up operations depending
         # on the temporary directory.
         if self.tempDir == None:
             self.tempDir = self.getDeviceRoot() + "/tmp"
             if (not self.dirExists(self.tempDir)):
                 return self.mkDir(self.tempDir)
 
         return self.tempDir
 
     def getAppRoot(self, packageName):
         """
         Returns the app root directory
+
         E.g /tests/fennec or /tests/firefox
-
-        returns:
-          success: path for app root
-          failure: None
         """
         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)
-        print "devicemanagerADB: getAppRoot failed"
-        return None
+        raise DMError("Failed to get application root for: %s" % packageName)
 
     def reboot(self, wait = False, **kwargs):
         """
         Reboots the device
-
-        returns:
-          success: status from test agent
-          failure: None
         """
-        ret = self._runCmd(["reboot"]).stdout.read()
+        self._runCmd(["reboot"])
         if (not wait):
-            return "Success"
+            return
         countdown = 40
         while (countdown > 0):
-            countdown
-            try:
-                self._checkCmd(["wait-for-device", "shell", "ls", "/sbin"])
-                return ret
-            except:
-                try:
-                    self._checkCmd(["root"])
-                except:
-                    time.sleep(1)
-                    print "couldn't get root"
-        return "Success"
+            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
-
-        returns:
-          success: text status from command or callback server
-          failure: None
+        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
-
-        returns:
-          success: time in ms
-          failure: None
         """
         timestr = self._runCmd(["shell", "date", "+%s"]).stdout.read().strip()
         if (not timestr or not timestr.isdigit()):
-            return None
+            raise DMError("Unable to get current time using date (got: '%s')" % timestr)
         return str(int(timestr)*1000)
 
     def recordLogcat(self):
         """
         Clears the logcat file making it easier to view specific events
         """
         # this does not require root privileges with ADB
         try:
@@ -788,34 +637,32 @@ class DeviceManagerADB(DeviceManager):
             return output.split('\r')
         except DMError, e:
             # to preserve compat with parent method, just ignore exceptions
             print "DeviceManager: Error recording logcat '%s'" % e.msg
             pass
 
     def getInfo(self, directive=None):
         """
-        Returns information about the device:
+        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
-        ### Note that uptimemillis is NOT supported, as there is no way to get this
-        ### data from the shell.
 
-        returns:
-           success: dict of info strings by directive name
-           failure: {}
+        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()
@@ -833,49 +680,41 @@ class DeviceManagerADB(DeviceManager):
         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 - ignored, but used for compatibility with SUTAgent
-
-        returns:
-          success: None
-          failure: DMError exception thrown
+        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':
-            return
-        raise DMError("uninstall failed for %s" % appName)
+        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 - ignored, but used for compatibility with SUTAgent
-
-        returns:
-          success: None
-          failure: DMError exception thrown
+        installPath - the path to where the application was installed (optional)
         """
-        results = self.uninstallApp(appName)
+        self.uninstallApp(appName)
         self.reboot()
         return
 
     def _runCmd(self, args):
         """
         Runs a command using adb
 
-        returns:
-          returncode from subprocess.Popen
+        returns: returncode from subprocess.Popen
         """
         finalArgs = [self.adbPath]
         if self.deviceSerial:
             finalArgs.extend(['-s', self.deviceSerial])
         # use run-as to execute commands as the package we're testing if
         # possible
         if not self.haveRootShell and self.useRunAs and args[0] == "shell" and args[1] != "run-as":
             args.insert(1, "run-as")
@@ -883,33 +722,31 @@ class DeviceManagerADB(DeviceManager):
         finalArgs.extend(args)
         return subprocess.Popen(finalArgs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
 
     def _runCmdAs(self, args):
         """
         Runs a command using adb
         If self.useRunAs is True, the command is run-as user specified in self.packageName
 
-        returns:
-          returncode from subprocess.Popen
+        returns: returncode from subprocess.Popen
         """
         if self.useRunAs:
             args.insert(1, "run-as")
             args.insert(2, self.packageName)
         return self._runCmd(args)
 
     # timeout is specified in seconds, and if no timeout is given,
     # we will run until we hit the default_timeout specified in the __init__
     def _checkCmd(self, args, timeout=None):
         """
         Runs a command using adb and waits for the command to finish.
         If timeout is specified, the process is killed after <timeout> seconds.
 
-        returns:
-          returncode from subprocess.Popen
+        returns: returncode from subprocess.Popen
         """
         # use run-as to execute commands as the package we're testing if
         # possible
         finalArgs = [self.adbPath]
         if self.deviceSerial:
             finalArgs.extend(['-s', self.deviceSerial])
         if not self.haveRootShell and self.useRunAs and args[0] == "shell" and args[1] != "run-as":
             args.insert(1, "run-as")
@@ -932,47 +769,41 @@ class DeviceManagerADB(DeviceManager):
         return ret_code
 
     def _checkCmdAs(self, args, timeout=None):
         """
         Runs a command using adb and waits for command to finish
         If self.useRunAs is True, the command is run-as user specified in self.packageName
         If timeout is specified, the process is killed after <timeout> seconds
 
-        returns:
-          returncode from subprocess.Popen
+        returns: returncode from subprocess.Popen
         """
         if (self.useRunAs):
             args.insert(1, "run-as")
             args.insert(2, self.packageName)
         return self._checkCmd(args, timeout)
 
     def chmodDir(self, remoteDir, mask="777"):
         """
         Recursively changes file permissions in a directory
-
-        returns:
-          success: True
-          failure: False
         """
-        if (self.isDir(remoteDir)):
+        if (self.dirExists(remoteDir)):
             files = self.listFiles(remoteDir.strip())
             for f in files:
                 remoteEntry = remoteDir.strip() + "/" + f.strip()
-                if (self.isDir(remoteEntry)):
+                if (self.dirExists(remoteEntry)):
                     self.chmodDir(remoteEntry)
                 else:
                     self._checkCmdAs(["shell", "chmod", mask, remoteEntry])
                     print "chmod " + remoteEntry
             self._checkCmdAs(["shell", "chmod", mask, remoteDir])
             print "chmod " + remoteDir
         else:
             self._checkCmdAs(["shell", "chmod", mask, remoteDir.strip()])
             print "chmod " + remoteDir.strip()
-        return True
 
     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)
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
@@ -7,60 +7,43 @@ import socket
 import SocketServer
 import time
 import os
 import re
 import posixpath
 import subprocess
 from threading import Thread
 import StringIO
-from devicemanager import DeviceManager, FileError, DMError, NetworkTools, _pop_last_line
+from devicemanager import DeviceManager, DMError, NetworkTools, _pop_last_line
 import errno
 from distutils.version import StrictVersion
 
-class AgentError(Exception):
-    "SUTAgent-specific exception."
-
-    def __init__(self, msg= '', fatal = False):
-        self.msg = msg
-        self.fatal = fatal
-
-    def __str__(self):
-        return self.msg
-
 class DeviceManagerSUT(DeviceManager):
     debug = 2
     tempRoot = os.getcwd()
     base_prompt = '$>'
     base_prompt_re = '\$\>'
     prompt_sep = '\x00'
     prompt_regex = '.*(' + base_prompt_re + prompt_sep + ')'
     agentErrorRE = re.compile('^##AGENT-WARNING##\ ?(.*)')
     default_timeout = 300
 
-    # TODO: member variable to indicate error conditions.
-    # This should be set to a standard error from the errno module.
-    # So, for example, when an error occurs because of a missing file/directory,
-    # before returning, the function would do something like 'self.error = errno.ENOENT'.
-    # The error would be set where appropriate--so sendCMD() could set socket errors,
-    # pushFile() and other file-related commands could set filesystem errors, etc.
-
     def __init__(self, host, port = 20701, retrylimit = 5, deviceRoot = None):
         self.host = host
         self.port = port
         self.retrylimit = retrylimit
         self._sock = None
         self.deviceRoot = deviceRoot
-        if self.getDeviceRoot() == None:
-            raise BaseException("Failed to connect to SUT Agent and retrieve the device root.")
-        try:
-            verstring = self._runCmds([{ 'cmd': 'ver' }])
-            self.agentVersion = re.sub('SUTAgentAndroid Version ', '', verstring)
-        except AgentError, err:
-            raise BaseException("Failed to get SUTAgent version")
+
+        # Initialize device root
+        self.getDeviceRoot()
+
+        # Get version
+        verstring = self._runCmds([{ 'cmd': 'ver' }])
+        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'),
@@ -71,17 +54,16 @@ class DeviceManagerSUT(DeviceManager):
             if (c.match(cmd)):
                 return False
 
         # If the command is not in our list, then it gets a response
         return True
 
     def _stripPrompt(self, data):
         """
-        internal function
         take a data blob and strip instances of the prompt '$>\x00'
         """
         promptre = re.compile(self.prompt_regex + '.*')
         retVal = []
         lines = data.split('\n')
         for line in lines:
             foundPrompt = False
             try:
@@ -127,31 +109,31 @@ class DeviceManagerSUT(DeviceManager):
         # one fails.  this is necessary in particular for pushFile(), where we don't want
         # to accidentally send extra data if a failure occurs during data transmission.
 
         retries = 0
         while retries < self.retrylimit:
             try:
                 self._doCmds(cmdlist, outputfile, timeout)
                 return
-            except AgentError, err:
+            except DMError, err:
                 # re-raise error if it's fatal (i.e. the device got the command but
                 # couldn't execute it). retry otherwise
                 if err.fatal:
                     raise err
                 if self.debug >= 2:
                     print err
                 retries += 1
                 # if we lost the connection or failed to establish one, wait a bit
                 if retries < self.retrylimit and not self._sock:
                     sleep_time = 5 * retries
                     print 'Could not connect; sleeping for %d seconds.' % sleep_time
                     time.sleep(sleep_time)
 
-        raise AgentError("Remote Device Error: unable to connect to %s after %s attempts" % (self.host, self.retrylimit))
+        raise DMError("Remote Device Error: unable to connect to %s after %s attempts" % (self.host, self.retrylimit))
 
     def _runCmds(self, cmdlist, timeout = None):
         """
         Similar to _sendCmds, but just returns any output as a string instead of
         writing to a file
         """
         outputfile = StringIO.StringIO()
         self._sendCmds(cmdlist, outputfile, timeout)
@@ -168,43 +150,43 @@ class DeviceManagerSUT(DeviceManager):
 
         if not self._sock:
             try:
                 if self.debug >= 1:
                     print "reconnecting socket"
                 self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
             except socket.error, msg:
                 self._sock = None
-                raise AgentError("Automation Error: unable to create socket: "+str(msg))
+                raise DMError("Automation Error: unable to create socket: "+str(msg))
 
             try:
                 self._sock.connect((self.host, int(self.port)))
                 if select.select([self._sock], [], [], timeout)[0]:
                     self._sock.recv(1024)
                 else:
-                    raise AgentError("Remote Device Error: Timeout in connecting", fatal=True)
+                    raise DMError("Remote Device Error: Timeout in connecting", fatal=True)
                     return False
             except socket.error, msg:
                 self._sock.close()
                 self._sock = None
-                raise AgentError("Remote Device Error: unable to connect socket: "+str(msg))
+                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 AgentError("ERROR: our cmd was %s bytes and we "
-                                                      "only sent %s" % (len(cmdline), sent))
+                    raise DMError("Remote Device Error: our cmd was %s bytes and we "
+                                  "only sent %s" % (len(cmdline), sent))
                 if cmd.get('data'):
                     sent = self._sock.send(cmd['data'])
                     if sent != len(cmd['data']):
-                            raise AgentError("ERROR: we had %s bytes of data to send, but "
-                                                              "only sent %s" % (len(cmd['data']), sent))
+                        raise DMError("Remote Device Error: we had %s bytes of data to send, but "
+                                      "only sent %s" % (len(cmd['data']), sent))
 
                 if self.debug >= 4:
                     print "sent cmd: " + str(cmd['cmd'])
             except socket.error, msg:
                 self._sock.close()
                 self._sock = None
                 if self.debug >= 1:
                     print "Remote Device Error: Error sending data to socket. cmd="+str(cmd['cmd'])+"; err="+str(msg)
@@ -236,28 +218,28 @@ class DeviceManagerSUT(DeviceManager):
                             if self.debug >= 4:
                                 print "response: " + str(temp)
                             timer = 0
                             if not temp:
                                 socketClosed = True
                                 errStr = 'connection closed'
                         timer += select_timeout
                         if timer > timeout:
-                            raise AgentError("Automation Error: Timeout in command %s" % cmd['cmd'], fatal=True)
+                            raise DMError("Automation Error: Timeout in command %s" % cmd['cmd'], fatal=True)
                     except socket.error, err:
                         socketClosed = True
                         errStr = str(err)
                         # This error shows up with we have our tegra rebooted.
                         if err[0] == errno.ECONNRESET:
                             errStr += ' - possible reboot'
 
                     if socketClosed:
                         self._sock.close()
                         self._sock = None
-                        raise AgentError("Automation Error: Error receiving data from socket. cmd=%s; err=%s" % (cmd, errStr))
+                        raise DMError("Automation Error: Error receiving data from socket. cmd=%s; err=%s" % (cmd, errStr))
 
                     data += temp
 
                     # If something goes wrong in the agent it will send back a string that
                     # starts with '##AGENT-WARNING##'
                     if not commandFailed:
                         errorMatch = self.agentErrorRE.match(data)
                         if errorMatch:
@@ -273,46 +255,41 @@ class DeviceManagerSUT(DeviceManager):
 
                     # periodically flush data to output file to make sure it doesn't get
                     # too big/unwieldly
                     if len(data) > 1024:
                             outputfile.write(data[0:1024])
                             data = data[1024:]
 
                 if commandFailed:
-                    raise AgentError("Automation Error: Agent Error processing command '%s'; err='%s'" %
-                                                      (cmd['cmd'], errorMatch.group(1)), fatal=True)
+                    raise DMError("Automation Error: Error processing command '%s'; err='%s'" %
+                                  (cmd['cmd'], errorMatch.group(1)), fatal=True)
 
                 # Write any remaining data to outputfile
                 outputfile.write(data)
 
         if shouldCloseSocket:
             try:
                 self._sock.close()
                 self._sock = None
             except:
                 self._sock = None
-                raise AgentError("Automation Error: Error closing socket")
+                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.
+        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
-
-        returns:
-          success: Return code from command
-          failure: None
         """
-
         cmdline = self._escapedCommandLine(cmd)
         if env:
             cmdline = '%s %s' % (self._formatEnvString(env), cmdline)
 
         haveExecSu = (StrictVersion(self.agentVersion) >= StrictVersion('1.13'))
 
         # Depending on agent version we send one of the following commands here:
         # * exec (run as normal user)
@@ -321,315 +298,206 @@ class DeviceManagerSUT(DeviceManager):
         # * execcwdsu (run as privileged user from specified directory)
 
         cmd = "exec"
         if cwd:
             cmd += "cwd"
         if root and haveExecSu:
             cmd += "su"
 
-        try:
-            if cwd:
-                self._sendCmds([{ 'cmd': '%s %s %s' % (cmd, cwd, cmdline) }], outputfile, timeout)
+        if cwd:
+            self._sendCmds([{ 'cmd': '%s %s %s' % (cmd, cwd, cmdline) }], outputfile, timeout)
+        else:
+            if (not root) or haveExecSu:
+                self._sendCmds([{ 'cmd': '%s %s' % (cmd, cmdline) }], outputfile, timeout)
             else:
-                if (not root) or haveExecSu:
-                    self._sendCmds([{ 'cmd': '%s %s' % (cmd, cmdline) }], outputfile, timeout)
-                else:
-                    # need to manually inject su -c for backwards compatibility (this may
-                    # not work on ICS or above!!)
-                    # (FIXME: this backwards compatibility code is really ugly and should
-                    # be deprecated at some point in the future)
-                    self._sendCmds([ { 'cmd': '%s su -c "%s"' % (cmd, cmdline) }], outputfile,
-                                                    timeout)
-        except AgentError:
-            return None
+                # need to manually inject su -c for backwards compatibility (this may
+                # not work on ICS or above!!)
+                # (FIXME: this backwards compatibility code is really ugly and should
+                # be deprecated at some point in the future)
+                self._sendCmds([ { 'cmd': '%s su -c "%s"' % (cmd, cmdline) }], outputfile,
+                               timeout)
 
         # dig through the output to get the return code
         lastline = _pop_last_line(outputfile)
         if lastline:
             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
-        return None
+        raise DMError("Automation Error: Error finding end of line/return value when running '%s'" % cmdline)
 
     def pushFile(self, localname, destname):
         """
         Copies localname from the host to destname on the device
-
-        returns:
-          success: True
-          failure: False
         """
-        if (os.name == "nt"):
-            destname = destname.replace('\\', '/')
-
-        if (self.debug >= 3):
-            print "in push file with: " + localname + ", and: " + destname
-        if (self.dirExists(destname)):
-            if (not destname.endswith('/')):
-                destname = destname + '/'
-            destname = destname + os.path.basename(localname)
-        if (self.validateFile(destname, localname) == True):
-            if (self.debug >= 3):
-                print "files are validated"
-            return True
-
-        if self.mkDirs(destname) == None:
-            print "Automation Error: unable to make dirs: " + destname
-            return False
-
-        if (self.debug >= 3):
-            print "sending: push " + destname
-
-        filesize = os.path.getsize(localname)
-        f = open(localname, 'rb')
-        data = f.read()
-        f.close()
+        self.mkDirs(destname)
 
         try:
-            retVal = self._runCmds([{ 'cmd': 'push ' + destname + ' ' + str(filesize),
-                                                              'data': data }])
-        except AgentError, e:
-            print "Automation Error: error pushing file: %s" % e.msg
-            return False
+            filesize = os.path.getsize(localname)
+            with open(localname, 'rb') as f:
+                remoteHash = self._runCmds([{ 'cmd': 'push ' + destname + ' ' + str(filesize),
+                                              'data': f.read() }]).strip()
+        except OSError:
+            raise DMError("DeviceManager: Error reading file to push")
 
         if (self.debug >= 3):
-            print "push returned: " + str(retVal)
+            print "push returned: %s" % hash
+
+        localHash = self._getLocalHash(localname)
 
-        validated = False
-        if (retVal):
-            retline = retVal.strip()
-            if (retline == None):
-                # Then we failed to get back a hash from agent, try manual validation
-                validated = self.validateFile(destname, localname)
-            else:
-                # Then we obtained a hash from push
-                localHash = self._getLocalHash(localname)
-                if (str(localHash) == str(retline)):
-                    validated = True
-        else:
-            # We got nothing back from sendCMD, try manual validation
-            validated = self.validateFile(destname, localname)
-
-        if (validated):
-            if (self.debug >= 3):
-                print "Push File Validated!"
-            return True
-        else:
-            if (self.debug >= 2):
-                print "Automation Error: Push File Failed to Validate!"
-            return False
+        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
-
-        returns:
-          success: directory name
-          failure: None
         """
-        if (self.dirExists(name)):
-            return name
-        else:
-            try:
-                retVal = self._runCmds([{ 'cmd': 'mkdr ' + name }])
-            except AgentError:
-                retVal = None
-            return retVal
+        if not self.dirExists(name):
+            self._runCmds([{ 'cmd': 'mkdr ' + name }])
 
     def pushDir(self, localDir, remoteDir):
         """
         Push localDir from host to remoteDir on the device
-
-        returns:
-          success: remoteDir
-          failure: None
         """
         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:
                 remoteRoot = remoteDir + '/' + parts[1]
                 if (remoteRoot.endswith('/')):
                     remoteName = remoteRoot + f
                 else:
                     remoteName = remoteRoot + '/' + f
+
                 if (parts[1] == ""):
                     remoteRoot = remoteDir
-                if (self.pushFile(os.path.join(root, f), remoteName) == False):
-                    # retry once
-                    self.removeFile(remoteName)
-                    if (self.pushFile(os.path.join(root, f), remoteName) == False):
-                        return None
-        return remoteDir
 
-    def dirExists(self, dirname):
-        """
-        Checks if dirname exists and is a directory
-        on the device file system
+                parent = os.path.dirname(remoteName)
+                if parent not in existentDirectories:
+                    self.mkDirs(remoteName)
+                    existentDirectories.append(parent)
 
-        returns:
-          success: True
-          failure: False
+                self.pushFile(os.path.join(root, f), remoteName)
+
+
+    def dirExists(self, remotePath):
         """
-        match = ".*" + dirname.replace('^', '\^') + "$"
-        dirre = re.compile(match)
-        try:
-            data = self._runCmds([ { 'cmd': 'cd ' + dirname }, { 'cmd': 'cwd' }])
-        except AgentError:
-            return False
+        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')
 
-        found = False
-        for d in data.splitlines():
-            if (dirre.match(d)):
-                found = True
+        return ret == 'TRUE'
 
-        return found
-
-    # Because we always have / style paths we make this a lot easier with some
-    # assumptions
     def fileExists(self, filepath):
         """
-        Checks if filepath exists and is a file on
-        the device file system
-
-        returns:
-          success: True
-          failure: False
+        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])
-        listfiles = self.listFiles(containingpath)
-        for f in listfiles:
-            if (f == s[-1]):
-                return True
-        return False
+        return s[-1] in self.listFiles(containingpath)
 
     def listFiles(self, rootdir):
         """
         Lists files on the device rootdir
 
-        returns:
-          success: array of filenames, ['file1', 'file2', ...]
-          failure: None
+        returns array of filenames, ['file1', 'file2', ...]
         """
         rootdir = rootdir.rstrip('/')
         if (self.dirExists(rootdir) == False):
             return []
-        try:
-            data = self._runCmds([{ 'cmd': 'cd ' + rootdir }, { 'cmd': 'ls' }])
-        except AgentError:
-            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
-
-        returns:
-          success: output of telnet
-          failure: None
         """
         if (self.debug>= 2):
             print "removing file: " + filename
-        try:
-            retVal = self._runCmds([{ 'cmd': 'rm ' + filename }])
-        except AgentError:
-            return None
-
-        return retVal
+        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
-
-        returns:
-          success: output of telnet
-          failure: None
         """
-        try:
-            retVal = self._runCmds([{ 'cmd': 'rmdr ' + remoteDir }])
-        except AgentError:
-            return None
-
-        return retVal
+        if self.dirExists(remoteDir):
+            self._runCmds([{ 'cmd': 'rmdr ' + remoteDir }])
 
     def getProcessList(self):
         """
         Lists the running processes on the device
 
-        returns:
-          success: array of process tuples
-          failure: []
+        returns: array of process tuples
         """
-        try:
-            data = self._runCmds([{ 'cmd': 'ps' }])
-        except AgentError:
-            return []
+        data = self._runCmds([{ 'cmd': 'ps' }])
 
         files = []
         for line in data.splitlines():
             if line:
                 pidproc = line.strip().split()
                 if (len(pidproc) == 2):
                     files += [[pidproc[0], pidproc[1]]]
                 elif (len(pidproc) == 3):
                     #android returns <userID> <procID> <procName>
                     files += [[pidproc[1], pidproc[2], pidproc[0]]]
         return files
 
     def fireProcess(self, appname, failIfRunning=False):
         """
-        DEPRECATED: Use shell() or launchApplication() for new code
+        Starts a process
+
+        returns: pid
 
-        returns:
-          success: pid
-          failure: None
+        DEPRECATED: Use shell() or launchApplication() for new code
         """
-        if (not appname):
-            if (self.debug >= 1):
-                print "WARNING: fireProcess called with no command to run"
-            return None
+        if not appname:
+            raise DMError("Automation Error: fireProcess called with no command to run")
 
         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):
-                return None
-
-        try:
-            self._runCmds([{ 'cmd': 'exec ' + appname }])
-        except AgentError:
-            return None
+                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.
-        process = self.processExist(appname)
+        pid = self.processExist(appname)
         if (self.debug >= 4):
-            print "got pid: %s for process: %s" % (process, appname)
-
-        return process
+            print "got pid: %s for process: %s" % (pid, appname)
+        return pid
 
     def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False):
         """
-        DEPRECATED: Use shell() or launchApplication() for new code
+        Launches a process, redirecting output to standard out
+
+        Returns output filename
 
-        returns:
-          success: output filename
-          failure: None
+        WARNING: Does not work how you expect on Android! The application's
+        own output will be flushed elsewhere.
+
+        DEPRECATED: Use shell() or launchApplication() for new code
         """
         if not cmd:
             if (self.debug >= 1):
                 print "WARNING: launchProcess called without command to run"
             return None
 
         cmdline = subprocess.list2cmdline(cmd)
         if (outputFile == "process.txt" or outputFile == None):
@@ -637,88 +505,60 @@ class DeviceManagerSUT(DeviceManager):
             if outputFile is None:
                 return None
             outputFile += "/process.txt"
             cmdline += " > " + outputFile
 
         # Prepend our env to the command
         cmdline = '%s %s' % (self._formatEnvString(env), cmdline)
 
-        if self.fireProcess(cmdline, failIfRunning) is None:
-            return None
+        # 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
+        Kills the process named appname
 
-        returns:
-          success: True
-          failure: False
+        If forceKill is True, process is killed regardless of state
         """
         if forceKill:
             print "WARNING: killProcess(): forceKill parameter unsupported on SUT"
-        try:
+        if self.processExist(appname):
             self._runCmds([{ 'cmd': 'kill ' + appname }])
-        except AgentError:
-            return False
-
-        return True
 
     def getTempDir(self):
         """
-        Gets the temporary directory we are using on this device
-        base on our device root, ensuring also that it exists.
+        Return a temporary directory on the device
 
-        returns:
-          success: path for temporary directory
-          failure: None
+        Will also ensure that directory exists
         """
-        try:
-            data = self._runCmds([{ 'cmd': 'tmpd' }])
-        except AgentError:
-            return None
-
-        return data.strip()
+        return self._runCmds([{ 'cmd': 'tmpd' }]).strip()
 
     def catFile(self, remoteFile):
         """
         Returns the contents of remoteFile
-
-        returns:
-          success: filecontents, string
-          failure: None
         """
-        try:
-            data = self._runCmds([{ 'cmd': 'cat ' + remoteFile }])
-        except AgentError:
-            return None
-
-        return data
+        return self._runCmds([{ 'cmd': 'cat ' + remoteFile }])
 
     def pullFile(self, remoteFile):
         """
         Returns contents of remoteFile using the "pull" command.
-
-        returns:
-          success: output of pullfile, string
-          failure: None
         """
         # 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
             print err_str
             self._sock = None
-            raise FileError(err_str)
+            raise DMError(err_str)
 
         # FIXME: We could possibly move these socket-reading functions up to
         # the class level if we wanted to refactor sendCMD().  For now they are
         # only used to pull files.
 
         def uread(to_recv, error_msg, timeout=None):
             """ unbuffered read """
             timer = 0
@@ -728,387 +568,262 @@ class DeviceManagerSUT(DeviceManager):
 
             try:
                 if select.select([self._sock], [], [], select_timeout)[0]:
                     data = self._sock.recv(to_recv)
                     timer = 0
                 timer += select_timeout
                 if timer > timeout:
                     err('timeout in uread while retrieving file')
-                    return None
 
                 if not data:
                     err(error_msg)
-                    return None
                 return data
             except:
                 err(error_msg)
-                return None
 
         def read_until_char(c, buf, error_msg):
             """ read until 'c' is found; buffer rest """
             while not '\n' in buf:
                 data = uread(1024, error_msg)
-                if data == None:
-                    err(error_msg)
-                    return ('', '', '')
                 buf += data
             return buf.partition(c)
 
         def read_exact(total_to_recv, buf, error_msg):
             """ read exact number of 'total_to_recv' bytes """
             while len(buf) < total_to_recv:
                 to_recv = min(total_to_recv - len(buf), 1024)
                 data = uread(to_recv, error_msg)
-                if data == None:
-                    return None
                 buf += data
             return buf
 
         prompt = self.base_prompt + self.prompt_sep
         buf = ''
 
         # expected return value:
         # <filename>,<filesize>\n<filedata>
         # or, if error,
         # <filename>,-1\n<error message>
-        try:
-            # just send the command first, we read the response inline below
-            self._runCmds([{ 'cmd': 'pull ' + remoteFile }])
-        except AgentError:
-            return None
+
+        # just send the command first, we read the response inline below
+        self._runCmds([{ 'cmd': 'pull ' + remoteFile }])
 
         # read metadata; buffer the rest
         metadata, sep, buf = read_until_char('\n', buf, 'could not find metadata')
         if not metadata:
             return None
         if self.debug >= 3:
             print 'metadata: %s' % metadata
 
         filename, sep, filesizestr = metadata.partition(',')
         if sep == '':
             err('could not find file size in returned metadata')
-            return None
         try:
             filesize = int(filesizestr)
         except ValueError:
             err('invalid file size in returned metadata')
-            return None
 
         if filesize == -1:
             # read error message
             error_str, sep, buf = read_until_char('\n', buf, 'could not find error message')
             if not error_str:
-                return None
+                err("blank error message")
             # prompt should follow
             read_exact(len(prompt), buf, 'could not find prompt')
             # failures are expected, so don't use "Remote Device Error" or we'll RETRY
-            print "DeviceManager: pulling file '%s' unsuccessful: %s" % (remoteFile, error_str)
-            return None
+            raise DMError("DeviceManager: pulling file '%s' unsuccessful: %s" % (remoteFile, error_str))
 
         # read file data
         total_to_recv = filesize + len(prompt)
         buf = read_exact(total_to_recv, buf, 'could not get all file data')
-        if buf == None:
-            return None
         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)
-
-        returns:
-          success: contents of file, string
-          failure: None
         """
         if localFile == '':
             localFile = os.path.join(self.tempRoot, "temp.txt")
 
-        try:
-            retVal = self.pullFile(remoteFile)
-        except:
-            return None
-
-        if (retVal is None):
-            return None
+        data = self.pullFile(remoteFile)
 
         fhandle = open(localFile, 'wb')
-        fhandle.write(retVal)
+        fhandle.write(data)
         fhandle.close()
         if not self.validateFile(remoteFile, localFile):
-            print 'DeviceManager: failed to validate file when downloading %s' % remoteFile
-            return None
-        return retVal
+            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)
-
-        returns:
-          success: list of files, string
-          failure: None
         """
         if (self.debug >= 2):
             print "getting files in '" + remoteDir + "'"
-        if checkDir:
-            try:
-                is_dir = self.isDir(remoteDir)
-            except FileError:
-                return None
-            if not is_dir:
-                return None
+        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):
             print filelist
         if not os.path.exists(localDir):
             os.makedirs(localDir)
 
         for f in filelist:
             if f == '.' or f == '..':
                 continue
             remotePath = remoteDir + '/' + f
             localPath = os.path.join(localDir, f)
-            try:
-                is_dir = self.isDir(remotePath)
-            except FileError:
-                print 'isdir failed on file "%s"; continuing anyway...' % remotePath
-                continue
-            if is_dir:
-                if (self.getDirectory(remotePath, localPath, False) == None):
-                    print 'Remote Device Error: failed to get directory "%s"' % remotePath
-                    return None
+            if self.dirExists(remotePath):
+                self.getDirectory(remotePath, localPath, False)
             else:
-                # It's sometimes acceptable to have getFile() return None, such as
-                # when the agent encounters broken symlinks.
-                # FIXME: This should be improved so we know when a file transfer really
-                # failed.
-                if self.getFile(remotePath, localPath) == None:
-                    print 'failed to get file "%s"; continuing anyway...' % remotePath
-        return filelist
-
-    def isDir(self, remotePath):
-        """
-        Checks if remotePath is a directory on the device
-
-        returns:
-          success: True
-          failure: False
-        """
-        try:
-            data = self._runCmds([{ 'cmd': 'isdir ' + remotePath }])
-        except AgentError:
-            # normally there should be no error here; a nonexistent file/directory will
-            # return the string "<filename>: No such file or directory".
-            # However, I've seen AGENT-WARNING returned before.
-            return False
-
-        retVal = data.strip()
-        if not retVal:
-            raise FileError('isdir returned null')
-        return retVal == 'TRUE'
+                self.getFile(remotePath, localPath)
 
     def validateFile(self, remoteFile, localFile):
         """
-        Checks if the remoteFile has the same md5 hash as the localFile
-
-        returns:
-          success: True
-          failure: False
+        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
-
-        returns:
-          success: MD5 hash for given filename
-          failure: None
         """
-        try:
-            data = self._runCmds([{ 'cmd': 'hash ' + filename }])
-        except AgentError:
-            return None
-
-        retVal = None
-        if data:
-            retVal = data.strip()
+        data = self._runCmds([{ 'cmd': 'hash ' + filename }]).strip()
         if self.debug >= 3:
-            print "remote hash returned: '%s'" % retVal
-        return retVal
+            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'
 
-        returns:
-          success: path for device root
-          failure: None
-        """
-        if self.deviceRoot:
-            deviceRoot = self.deviceRoot
-        else:
-            try:
-                data = self._runCmds([{ 'cmd': 'testroot' }])
-            except:
-                return None
+        if not self.dirExists(self.deviceRoot):
+            self.mkDir(self.deviceRoot)
 
-            deviceRoot = data.strip() + '/tests'
-
-        if (not self.dirExists(deviceRoot)):
-            if (self.mkDir(deviceRoot) == None):
-                return None
-
-        self.deviceRoot = deviceRoot
         return self.deviceRoot
 
     def getAppRoot(self, packageName):
         """
         Returns the app root directory
+
         E.g /tests/fennec or /tests/firefox
-
-        returns:
-          success: path for app root
-          failure: None
         """
-        try:
-            data = self._runCmds([{ 'cmd': 'getapproot ' + packageName }])
-        except:
-            return None
+        data = self._runCmds([{ 'cmd': 'getapproot ' + packageName }])
 
         return data.strip()
 
     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
         """
         devroot = self.getDeviceRoot()
         if (devroot == None):
             return None
 
         # 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)
 
         if dest_dir[-1] != '/':
             dest_dir += '/'
 
-        try:
-            data = self._runCmds([{ 'cmd': 'unzp %s %s' % (file_path, dest_dir)}])
-        except AgentError:
-            return None
-
-        return data
+        self._runCmds([{ 'cmd': 'unzp %s %s' % (file_path, dest_dir)}])
 
     def reboot(self, ipAddr=None, port=30000):
         """
         Reboots the device
-
-        returns:
-          success: status from test agent
-          failure: None
         """
         cmd = 'rebt'
 
         if (self.debug > 3):
             print "INFO: sending rebt command"
 
         if (ipAddr is not None):
         #create update.info file:
-            try:
-                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 }])
-            except AgentError:
-                return None
+            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 }])
 
             ip, port = self._getCallbackIpAndPort(ipAddr, port)
             cmd += " %s %s" % (ip, port)
             # Set up our callback server
             callbacksvr = callbackServer(ip, port, self.debug)
 
-        try:
-            status = self._runCmds([{ 'cmd': cmd }])
-        except AgentError:
-            return None
+        status = self._runCmds([{ 'cmd': cmd }])
 
         if (ipAddr is not None):
             status = callbacksvr.disconnect()
 
         if (self.debug > 3):
             print "INFO: rebt- got status back: " + str(status)
-        return status
 
     def getInfo(self, directive=None):
         """
-        Returns information about the device:
+        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:
-          success: dict of info strings by directive name
-          failure: None
+        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']
         if (directive in directives):
             directives = [directive]
 
         for d in directives:
-            try:
-                data = self._runCmds([{ 'cmd': 'info ' + d }])
-            except AgentError:
-                return result
+            data = self._runCmds([{ 'cmd': 'info ' + d }])
 
-            if (data is None):
-                continue
             data = collapseSpaces.sub(' ', data)
             result[d] = data.split('\n')
 
         # Get rid of any 0 length members of the arrays
         for k, v in result.iteritems():
             result[k] = filter(lambda x: x != '', result[k])
 
         # Format the process output
@@ -1121,100 +836,77 @@ class DeviceManagerSUT(DeviceManager):
 
         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)
-
-        returns:
-          success: None
-          failure: error string
         """
         cmd = 'inst ' + appBundlePath
         if destPath:
             cmd += ' ' + destPath
 
-        try:
-            data = self._runCmds([{ 'cmd': cmd }])
-        except AgentError, err:
-            print "Remote Device Error: Error installing app: %s" % err
-            return "%s" % err
+        data = self._runCmds([{ 'cmd': cmd }])
 
         f = re.compile('Failure')
         for line in data.split():
             if (f.match(line)):
-                return line
-        return None
+                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)
-
-        returns:
-          success: None
-          failure: DMError exception thrown
         """
         cmd = 'uninstall ' + appName
         if installPath:
             cmd += ' ' + installPath
-        try:
-            data = self._runCmds([{ 'cmd': cmd }])
-        except AgentError, err:
-            raise DMError("Remote Device Error: Error uninstalling all %s" % appName)
+        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)
-
-        returns:
-          success: None
-          failure: DMError exception thrown
         """
         cmd = 'uninst ' + appName
         if installPath:
             cmd += ' ' + installPath
-        try:
-            data = self._runCmds([{ 'cmd': cmd }])
-        except AgentError:
-            raise DMError("Remote Device Error: uninstall failed for %s" % appName)
+        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
-
-        returns:
-          success: text status from command or callback server
-          failure: None
         """
         status = None
         cmd = 'updt '
         if (processName == None):
             # Then we pass '' for processName
             cmd += "'' " + appBundlePath
         else:
             cmd += processName + ' ' + appBundlePath
@@ -1226,83 +918,69 @@ class DeviceManagerSUT(DeviceManager):
             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):
             print "INFO: updateApp using command: " + str(cmd)
 
-        try:
-            status = self._runCmds([{ 'cmd': cmd }])
-        except AgentError:
-            return None
+        status = self._runCmds([{ 'cmd': cmd }])
 
         if ipAddr is not None:
             status = callbacksvr.disconnect()
 
         if (self.debug >= 3):
             print "INFO: updateApp: got status back: " + str(status)
 
-        return status
-
     def getCurrentTime(self):
         """
         Returns device time in milliseconds since the epoch
-
-        returns:
-          success: time in ms
-          failure: None
         """
-        try:
-            data = self._runCmds([{ 'cmd': 'clok' }])
-        except AgentError:
-            return None
-
-        return data.strip()
+        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.
+        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!
         """
         ip = aIp
         nettools = NetworkTools()
         if (ip == None):
             ip = nettools.getLanIp()
         if (aPort != None):
             port = nettools.findOpenPort(ip, aPort)
         else:
             port = nettools.findOpenPort(ip, 30000)
         return ip, port
 
     def _formatEnvString(self, env):
         """
         Returns a properly formatted env string for the agent.
+
         Input - env, which is either None, '', or a dict
         Output - a quoted string of the form: '"envvar1=val1,envvar2=val2..."'
         If env is None or '' return '' (empty quoted string)
         """
         if (env == None or env == ''):
             return ''
 
         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
+
         NOTE: this only works on a tegra ATM
-        return:
-          success: True
-          failure: False
 
         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"
             return False
 
@@ -1327,37 +1005,25 @@ class DeviceManagerSUT(DeviceManager):
         if (width < 100 or width > 9999):
             return False
 
         if (height < 100 or height > 9999):
             return False
 
         if (self.debug >= 3):
             print "INFO: adjusting screen resolution to %s, %s and rebooting" % (width, height)
-        try:
-            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) }])
-        except AgentError:
-            return False
 
-        return True
+        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
-
-        returns:
-          success: True
-          failure: False
         """
-        try:
-            self._runCmds([{ 'cmd': "chmod "+remoteDir }])
-        except AgentError:
-            return False
-        return True
+        self._runCmds([{ 'cmd': "chmod "+remoteDir }])
 
 gCallbackData = ''
 
 class myServer(SocketServer.TCPServer):
     allow_reuse_address = True
 
 class callbackServer():
     def __init__(self, ip, port, debuglevel):
--- a/testing/mozbase/mozdevice/mozdevice/sutcli.py
+++ b/testing/mozbase/mozdevice/mozdevice/sutcli.py
@@ -77,17 +77,17 @@ class SUTCli(object):
                           'rmdir': { 'function': lambda d: self.dm.removeDir(d),
                                     'min_args': 1,
                                     'max_args': 1,
                                     'help_args': '<remote>',
                                     'help': 'recursively remove directory from device'
                                 }
                           }
 
-        for (commandname, command) in self.commands.iteritems():
+        for (commandname, command) in sorted(self.commands.iteritems()):
             help_args = command['help_args']
             usage += "  %s - %s\n" % (" ".join([ commandname,
                                                  help_args ]).rstrip(),
                                       command['help'])
         self.parser = OptionParser(usage)
         self.add_options(self.parser)
 
         (self.options, self.args) = self.parser.parse_args(args)
--- a/testing/mozbase/mozdevice/tests/manifest.ini
+++ b/testing/mozbase/mozdevice/tests/manifest.ini
@@ -1,1 +1,4 @@
-[sut.py]
+[sut_basic.py]
+[sut_mkdir.py]
+[sut_push.py]
+[sut_pull.py]
--- a/testing/mozbase/mozdevice/tests/sut.py
+++ b/testing/mozbase/mozdevice/tests/sut.py
@@ -1,134 +1,62 @@
 #!/usr/bin/env python
 
 # Any copyright is dedicated to the Public Domain.
 # http://creativecommons.org/publicdomain/zero/1.0/
 
 import socket
-import mozdevice
 from threading import Thread
 import unittest
-import sys
 import time
 
-class BasicTest(unittest.TestCase):
+class MockAgent(object):
+    def __init__(self, tester, start_commands = None, commands = []):
+        if start_commands:
+            self.commands = start_commands
+        else:
+            self.commands = [("testroot", "/mnt/sdcard"),
+                                   ("isdir /mnt/sdcard/tests", "TRUE"),
+                                   ("ver", "SUTAgentAndroid Version 1.14")]
+        self.commands = self.commands + commands
+
+        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self._sock.bind(("127.0.0.1", 0))
+        self._sock.listen(1)
+
+        self.tester = tester
+
+        self.thread = Thread(target=self._serve_thread)
+        self.thread.start()
+
+    @property
+    def port(self):
+        return self._sock.getsockname()[1]
 
     def _serve_thread(self):
         conn = None
         while self.commands:
             if not conn:
                 conn, addr = self._sock.accept()
                 conn.send("$>\x00")
             (command, response) = self.commands.pop(0)
             data = conn.recv(1024).strip()
-            self.assertEqual(data, command)
+            self.tester.assertEqual(data, command)
             # send response and prompt separately to test for bug 789496
             # FIXME: Improve the mock agent, since overloading the meaning
             # of 'response' is getting confusing.
             if response is None:
                 conn.shutdown(socket.SHUT_RDWR)
                 conn.close()
                 conn = None
             elif type(response) is int:
                 time.sleep(response)
             else:
-                conn.send("%s\n" % response)
+                # pull is handled specially, as we just pass back the full
+                # command line
+                if "pull" in command:
+                    conn.send(response)
+                else:
+                    conn.send("%s\n" % response)
                 conn.send("$>\x00")
 
-    def _serve(self, commands):
-        self.commands = commands
-        thread = Thread(target=self._serve_thread)
-        thread.start()
-        return thread
-
-    def test_init(self):
-        """Tests DeviceManager initialization."""
-        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self._sock.bind(("127.0.0.1", 0))
-        self._sock.listen(1)
-
-        thread = self._serve([("testroot", "/mnt/sdcard"),
-                              ("cd /mnt/sdcard/tests", ""),
-                              ("cwd", "/mnt/sdcard/tests"),
-                              ("ver", "SUTAgentAndroid Version XX")])
-
-        port = self._sock.getsockname()[1]
-        mozdevice.DroidSUT.debug = 4
-        d = mozdevice.DroidSUT("127.0.0.1", port=port)
-        thread.join()
-
-    def test_reconnect(self):
-        """Tests DeviceManager initialization."""
-        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self._sock.bind(("127.0.0.1", 0))
-        self._sock.listen(1)
-
-        thread = self._serve([("testroot", "/mnt/sdcard"),
-                              ("cd /mnt/sdcard/tests", ""),
-                              ("cwd", None),
-                              ("cd /mnt/sdcard/tests", ""),
-                              ("cwd", "/mnt/sdcard/tests"),
-                              ("ver", "SUTAgentAndroid Version XX")])
-
-        port = self._sock.getsockname()[1]
-        mozdevice.DroidSUT.debug = 4
-        d = mozdevice.DroidSUT("127.0.0.1", port=port)
-        thread.join()
-
-    def test_err(self):
-        """Tests error handling during initialization."""
-        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self._sock.bind(("127.0.0.1", 0))
-        self._sock.listen(1)
-
-        thread = self._serve([("testroot", "/mnt/sdcard"),
-                              ("cd /mnt/sdcard/tests", "##AGENT-WARNING## no such file or directory"),
-                              ("cd /mnt/sdcard/tests", "##AGENT-WARNING## no such file or directory"),
-                              ("mkdr /mnt/sdcard/tests", "/mnt/sdcard/tests successfully created"),
-                              ("ver", "SUTAgentAndroid Version XX")])
-
-        port = self._sock.getsockname()[1]
-        mozdevice.DroidSUT.debug = 4
-        dm = mozdevice.DroidSUT("127.0.0.1", port=port)
-        thread.join()
-
-    def test_timeout_normal(self):
-        """Tests DeviceManager timeout, normal case."""
-        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self._sock.bind(("127.0.0.1", 0))
-        self._sock.listen(1)
-
-        thread = self._serve([("testroot", "/mnt/sdcard"),
-                              ("cd /mnt/sdcard/tests", ""),
-                              ("cwd", "/mnt/sdcard/tests"),
-                              ("ver", "SUTAgentAndroid Version XX"),
-                              ("rm /mnt/sdcard/tests/test.txt", "Removed the file")])
-
-        port = self._sock.getsockname()[1]
-        mozdevice.DroidSUT.debug = 4
-        d = mozdevice.DroidSUT("127.0.0.1", port=port)
-        data = d.removeFile('/mnt/sdcard/tests/test.txt')
-        self.assertEqual(data, "Removed the file")
-        thread.join()
-
-    def test_timeout_timeout(self):
-        """Tests DeviceManager timeout, timeout case."""
-        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        self._sock.bind(("127.0.0.1", 0))
-        self._sock.listen(1)
-
-        thread = self._serve([("testroot", "/mnt/sdcard"),
-                              ("cd /mnt/sdcard/tests", ""),
-                              ("cwd", "/mnt/sdcard/tests"),
-                              ("ver", "SUTAgentAndroid Version XX"),
-                              ("rm /mnt/sdcard/tests/test.txt", 3)])
-
-        port = self._sock.getsockname()[1]
-        mozdevice.DroidSUT.debug = 4
-        d = mozdevice.DroidSUT("127.0.0.1", port=port)
-        d.default_timeout = 1
-        data = d.removeFile('/mnt/sdcard/tests/test.txt')
-        self.assertEqual(data, None)
-        thread.join()
-
-if __name__ == '__main__':
-    unittest.main()
+    def wait(self):
+        self.thread.join()