Bug 799605 - Mirror mozbase -> m-c for bug 799288 and bug 799507 @ https://github.com/mozilla/mozbase/commit/ 36a2f63be33af799a54d7f1511dc922730b10b22 ; r=wlach, a=approval-mozilla-aurora
authorJonathan Griffin <jgriffin@mozilla.com>
Mon, 15 Oct 2012 15:40:53 -0700
changeset 113381 d5fd58f26db5b77b72b169ccbdcc0270b0fc6e4f
parent 113380 5d4dac6cfded52278026a89b2b9e858ff2eac4fa
child 113382 e0979fb694aa4ea63b839fdbf8eac016b48c0229
push idunknown
push userunknown
push dateunknown
reviewerswlach, approval-mozilla-aurora
bugs799605, 799288, 799507
milestone18.0a2
Bug 799605 - Mirror mozbase -> m-c for bug 799288 and bug 799507 @ https://github.com/mozilla/mozbase/commit/ 36a2f63be33af799a54d7f1511dc922730b10b22 ; r=wlach, a=approval-mozilla-aurora
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/setup.py
testing/mozbase/mozdevice/tests/manifest.ini
testing/mozbase/mozdevice/tests/sut.py
testing/mozbase/mozdevice/tests/sut_basic.py
testing/mozbase/mozdevice/tests/sut_mkdir.py
testing/mozbase/mozdevice/tests/sut_ps.py
testing/mozbase/mozdevice/tests/sut_pull.py
testing/mozbase/mozdevice/tests/sut_push.py
testing/mozbase/mozhttpd/setup.py
testing/mozbase/mozinfo/mozinfo/mozinfo.py
testing/mozbase/mozinfo/setup.py
testing/mozbase/mozinstall/mozinstall/mozinstall.py
testing/mozbase/mozinstall/setup.py
testing/mozbase/mozlog/mozlog/logger.py
testing/mozbase/mozprocess/mozprocess/processhandler.py
testing/mozbase/mozrunner/setup.py
--- a/testing/mozbase/mozdevice/mozdevice/devicemanager.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
@@ -1,17 +1,19 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import hashlib
 import socket
 import os
 import re
+import struct
 import StringIO
+import zlib
 
 class DMError(Exception):
     "generic devicemanager exception."
 
     def __init__(self, msg= '', fatal = False):
         self.msg = msg
         self.fatal = fatal
 
@@ -144,16 +146,18 @@ class DeviceManager:
     def processExist(self, appname):
         """
         Iterates process list and checks if pid exists
 
         returns:
           success: pid
           failure: None
         """
+        if not isinstance(appname, basestring):
+            raise TypeError("appname %s is not a string" % appname)
 
         pid = None
 
         #filter out extra spaces
         parts = filter(lambda x: x != '', appname.split(' '))
         appname = ' '.join(parts)
 
         #filter out the quoted env string if it exists
@@ -507,16 +511,60 @@ class DeviceManager:
           failure: None
         """
         buf = StringIO.StringIO()
         if self.shell(["/system/bin/logcat", "-d", "dalvikvm:S", "ConnectivityService:S", "WifiMonitor:S", "WifiStateTracker:S", "wpa_supplicant:S", "NetworkStateTracker:S"], buf, root=True) != 0:
             return None
 
         return str(buf.getvalue()[0:-1]).rstrip().split('\r')
 
+    @staticmethod
+    def _writePNG(buf, width, height):
+        """
+        Method for writing a PNG from a buffer, used by getScreenshot on older devices
+        Based on: http://code.activestate.com/recipes/577443-write-a-png-image-in-native-python/
+        """
+        width_byte_4 = width * 4
+        raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in range(0, (height - 1) * width * 4, width_byte_4))
+        def png_pack(png_tag, data):
+            chunk_head = png_tag + data
+            return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+        return b"".join([
+                b'\x89PNG\r\n\x1a\n',
+                png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
+                png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+                png_pack(b'IEND', b'')])
+
+    def saveScreenshot(self, filename):
+        """
+        Takes a screenshot of what's being display on the device. Uses
+        "screencap" on newer (Android 3.0+) devices (and some older ones with
+        the functionality backported). This function also works on B2G.
+
+        Throws an exception on failure. This will always fail on devices
+        without the screencap utility.
+        """
+        screencap = '/system/bin/screencap'
+        if not self.fileExists(screencap):
+            raise DMError("Unable to capture screenshot on device: no screencap utility")
+
+        with open(filename, 'w') as pngfile:
+            # newer versions of screencap can write directly to a png, but some
+            # older versions can't
+            tempScreenshotFile = self.getDeviceRoot() + "/ss-dm.tmp"
+            self.shellCheckOutput(["sh", "-c", "%s > %s" %
+                                   (screencap, tempScreenshotFile)],
+                                  root=True)
+            buf = self.pullFile(tempScreenshotFile)
+            width = int(struct.unpack("I", buf[0:4])[0])
+            height = int(struct.unpack("I", buf[4:8])[0])
+            with open(filename, 'w') as pngfile:
+                pngfile.write(self._writePNG(buf[12:], width, height))
+            self.removeFile(tempScreenshotFile)
+
     @abstractmethod
     def chmodDir(self, remoteDir, mask="777"):
         """
         Recursively changes file permissions in a directory
 
         returns:
           success: True
           failure: False
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
@@ -178,23 +178,19 @@ class DeviceManagerADB(DeviceManager):
             return True
         except:
             raise DMError("Error pushing file to device")
 
     def mkDir(self, name):
         """
         Creates a single directory on the device file system
         """
-        try:
-            result = self._runCmdAs(["shell", "mkdir", name]).stdout.read()
-            if 'read-only file system' in result.lower():
-                raise DMError("Error creating directory: read only file system")
-            # otherwise assume success
-        except:
-            raise DMError("Error creating directory")
+        result = self._runCmdAs(["shell", "mkdir", name]).stdout.read()
+        if 'read-only file system' in result.lower():
+            raise DMError("Error creating directory: read only file system")
 
     def pushDir(self, localDir, remoteDir):
         """
         Push localDir from host to remoteDir on the device
         """
         # adb "push" accepts a directory as an argument, but if the directory
         # contains symbolic links, the links are pushed, rather than the linked
         # files; we either zip/unzip or push file-by-file to get around this
@@ -319,17 +315,17 @@ class DeviceManagerADB(DeviceManager):
         """
         p = self._runCmd(["shell", "ps"])
             # first line is the headers
         p.stdout.readline()
         proc = p.stdout.readline()
         ret = []
         while (proc):
             els = proc.split()
-            ret.append(list([els[1], els[len(els) - 1], els[0]]))
+            ret.append(list([int(els[1]), els[len(els) - 1], els[0]]))
             proc =  p.stdout.readline()
         return ret
 
     def fireProcess(self, appname, failIfRunning=False):
         """
         Starts a process
 
         returns: pid
@@ -509,26 +505,30 @@ class DeviceManagerADB(DeviceManager):
 
         # /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')]:
+        paths = [('/mnt/sdcard', 'tests'),
+                 ('/data/local', 'tests')]
+        for (basePath, subPath) in paths:
             if self.dirExists(basePath):
                 testRoot = os.path.join(basePath, subPath)
-                if self.mkDir(testRoot):
+                try:
+                    self.mkDir(testRoot)
                     self.deviceRoot = testRoot
                     return
+                except:
+                    pass
 
-        raise DMError("Unable to set up device root as /mnt/sdcard/tests "
-                                    "or /data/local/tests")
+        raise DMError("Unable to set up device root using paths: [%s]"
+                        % ", ".join(["'%s'" % os.path.join(b, s) for b, s in paths]))
 
     def getDeviceRoot(self):
         """
         Gets the device root for the testing area on the device
 
         For all devices we will use / type slashes and depend on the device-agent
         to sort those out.  The agent will return us the device location where we
         should store things, we will then create our /tests structure relative to
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
@@ -114,17 +114,17 @@ class DeviceManagerSUT(DeviceManager):
             try:
                 self._doCmds(cmdlist, outputfile, timeout)
                 return
             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:
+                if self.debug >= 4:
                     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)
 
@@ -440,26 +440,26 @@ class DeviceManagerSUT(DeviceManager):
     def getProcessList(self):
         """
         Lists the running processes on the device
 
         returns: array of process tuples
         """
         data = self._runCmds([{ 'cmd': 'ps' }])
 
-        files = []
+        processTuples = []
         for line in data.splitlines():
             if line:
                 pidproc = line.strip().split()
                 if (len(pidproc) == 2):
-                    files += [[pidproc[0], pidproc[1]]]
+                    processTuples += [[pidproc[0], pidproc[1]]]
                 elif (len(pidproc) == 3):
                     #android returns <userID> <procID> <procName>
-                    files += [[pidproc[1], pidproc[2], pidproc[0]]]
-        return files
+                    processTuples += [[int(pidproc[1]), pidproc[2], int(pidproc[0])]]
+        return processTuples
 
     def fireProcess(self, appname, failIfRunning=False):
         """
         Starts a process
 
         returns: pid
 
         DEPRECATED: Use shell() or launchApplication() for new code
--- a/testing/mozbase/mozdevice/mozdevice/sutcli.py
+++ b/testing/mozbase/mozdevice/mozdevice/sutcli.py
@@ -74,17 +74,24 @@ class SUTCli(object):
                                     'help_args': '<remote>',
                                     'help': 'remove file from device'
                                 },
                           'rmdir': { 'function': lambda d: self.dm.removeDir(d),
                                     'min_args': 1,
                                     'max_args': 1,
                                     'help_args': '<remote>',
                                     'help': 'recursively remove directory from device'
-                                }
+                                },
+                          'screencap': { 'function': lambda f: self.dm.saveScreenshot(f),
+                                          'min_args': 1,
+                                          'max_args': 1,
+                                          'help_args': '<png file>',
+                                          'help': 'capture screenshot of device in action'
+                                          }
+
                           }
 
         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)
--- a/testing/mozbase/mozdevice/setup.py
+++ b/testing/mozbase/mozdevice/setup.py
@@ -1,16 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 from setuptools import setup
 
-PACKAGE_VERSION = '0.9'
+PACKAGE_VERSION = '0.11'
 
 # take description from README
 here = os.path.dirname(os.path.abspath(__file__))
 try:
     description = file(os.path.join(here, 'README.md')).read()
 except (OSError, IOError):
     description = ''
 
--- a/testing/mozbase/mozdevice/tests/manifest.ini
+++ b/testing/mozbase/mozdevice/tests/manifest.ini
@@ -1,4 +1,5 @@
 [sut_basic.py]
 [sut_mkdir.py]
 [sut_push.py]
 [sut_pull.py]
+[sut_ps.py]
--- a/testing/mozbase/mozdevice/tests/sut.py
+++ b/testing/mozbase/mozdevice/tests/sut.py
@@ -1,26 +1,25 @@
 #!/usr/bin/env python
 
 # Any copyright is dedicated to the Public Domain.
 # http://creativecommons.org/publicdomain/zero/1.0/
 
 import socket
 from threading import Thread
-import unittest
 import time
 
 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")]
+                             ("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
 
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_basic.py
@@ -0,0 +1,76 @@
+from sut import MockAgent
+import mozdevice
+import unittest
+
+class BasicTest(unittest.TestCase):
+
+    def test_init(self):
+        """Tests DeviceManager initialization."""
+        a = MockAgent(self)
+
+        mozdevice.DroidSUT.debug = 4
+        d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        # all testing done in device's constructor
+        a.wait()
+
+    def test_init_err(self):
+        """Tests error handling during initialization."""
+        cmds = [("testroot", "/mnt/sdcard"),
+                ("isdir /mnt/sdcard/tests", "/mnt/sdcard/tests: No such file or directory\n"),
+                ("isdir /mnt/sdcard/tests", "/mnt/sdcard/tests: No such file or directory\n"),
+                ("mkdr /mnt/sdcard/tests", "/mnt/sdcard/tests successfully created"),
+                ("ver", "SUTAgentAndroid Version 1.14")]
+        a = MockAgent(self, start_commands = cmds)
+        mozdevice.DroidSUT.debug = 4
+        dm = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        a.wait()
+
+    def test_timeout_normal(self):
+        """Tests DeviceManager timeout, normal case."""
+        a = MockAgent(self, commands = [("isdir /mnt/sdcard/tests", "TRUE"),
+                                        ("cd /mnt/sdcard/tests", ""),
+                                        ("ls", "test.txt"),
+                                        ("rm /mnt/sdcard/tests/test.txt",
+                                         "Removed the file")])
+        mozdevice.DroidSUT.debug = 4
+        d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        ret = d.removeFile('/mnt/sdcard/tests/test.txt')
+        self.assertEqual(ret, None) # if we didn't throw an exception, we're ok
+        a.wait()
+
+    def test_timeout_timeout(self):
+        """Tests DeviceManager timeout, timeout case."""
+        a = MockAgent(self, commands = [("isdir /mnt/sdcard/tests", "TRUE"),
+                                        ("cd /mnt/sdcard/tests", ""),
+                                        ("ls", "test.txt"),
+                                        ("rm /mnt/sdcard/tests/test.txt", 0)])
+        mozdevice.DroidSUT.debug = 4
+        d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        d.default_timeout = 1
+        exceptionThrown = False
+        try:
+            d.removeFile('/mnt/sdcard/tests/test.txt')
+        except mozdevice.DMError:
+            exceptionThrown = True
+        self.assertEqual(exceptionThrown, True)
+        a.wait()
+
+    def test_shell(self):
+        """Tests shell command"""
+        for cmd in [ ("exec foobar", False), ("execsu foobar", True) ]:
+            for retcode in [ 1, 2 ]:
+                a = MockAgent(self, commands=[(cmd[0],
+                                               "\nreturn code [%s]" % retcode)])
+                d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+                exceptionThrown = False
+                try:
+                    d.shellCheckOutput(["foobar"], root=cmd[1])
+                except mozdevice.DMError:
+                    exceptionThrown = True
+                expectedException = (retcode != 0)
+                self.assertEqual(exceptionThrown, expectedException)
+
+                a.wait()
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_mkdir.py
@@ -0,0 +1,39 @@
+from sut import MockAgent
+import mozdevice
+import unittest
+
+class PushTest(unittest.TestCase):
+
+    def test_mkdirs(self):
+        subTests = [ { 'cmds': [ ("isdir /mnt", "TRUE"),
+                                 ("isdir /mnt/sdcard", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "FALSE"),
+                                 ("mkdr /mnt/sdcard/baz",
+                                  "/mnt/sdcard/baz successfully created"),
+                                 ("isdir /mnt/sdcard/baz/boop", "FALSE"),
+                                 ("mkdr /mnt/sdcard/baz/boop",
+                                  "/mnt/sdcard/baz/boop successfully created") ],
+                       'expectException': False },
+                     { 'cmds': [ ("isdir /mnt", "TRUE"),
+                                 ("isdir /mnt/sdcard", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "FALSE"),
+                                 ("mkdr /mnt/sdcard/baz",
+                                  "##AGENT-WARNING## Could not create the directory /mnt/sdcard/baz") ],
+                       'expectException': True },
+                     ]
+        for subTest in subTests:
+            a = MockAgent(self, commands = subTest['cmds'])
+
+            exceptionThrown = False
+            try:
+                mozdevice.DroidSUT.debug = 4
+                d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+                d.mkDirs("/mnt/sdcard/baz/boop/bip")
+            except mozdevice.DMError, e:
+                exceptionThrown = True
+            self.assertEqual(exceptionThrown, subTest['expectException'])
+
+            a.wait()
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_ps.py
@@ -0,0 +1,31 @@
+from sut import MockAgent
+import mozdevice
+import unittest
+
+class PsTest(unittest.TestCase):
+
+    pscommands = [('ps',
+                   "10029	549	com.android.launcher\n"
+                   "10066	1198	com.twitter.android")]
+
+    def test_processList(self):
+        a = MockAgent(self,
+                      commands=self.pscommands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        pslist = d.getProcessList()
+        self.assertEqual(len(pslist), 2)
+        self.assertEqual(pslist[0], [549, 'com.android.launcher', 10029])
+        self.assertEqual(pslist[1], [1198, 'com.twitter.android', 10066])
+
+        a.wait()
+
+    def test_processExist(self):
+        for i in [('com.android.launcher', 549),
+                  ('com.fennec.android', None)]:
+            a = MockAgent(self, commands=self.pscommands)
+            d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+            self.assertEqual(d.processExist(i[0]), i[1])
+            a.wait()
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_pull.py
@@ -0,0 +1,49 @@
+from sut import MockAgent
+import mozdevice
+import unittest
+import hashlib
+import tempfile
+import os
+
+class PullTest(unittest.TestCase):
+
+    def test_pull_success(self):
+        for count in [ 1, 4, 1024, 2048 ]:
+            cheeseburgers = ""
+            for i in range(count):
+                cheeseburgers += "cheeseburgers"
+
+            # pull file is kind of gross, make sure we can still execute commands after it's done
+            remoteName = "/mnt/sdcard/cheeseburgers"
+            mozdevice.DroidSUT.debug = 4
+            a = MockAgent(self, commands = [("pull %s" % remoteName,
+                                             "%s,%s\n%s" % (remoteName,
+                                                            len(cheeseburgers),
+                                                            cheeseburgers)),
+                                            ("isdir /mnt/sdcard", "TRUE")])
+
+            d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+            pulledData = d.pullFile("/mnt/sdcard/cheeseburgers")
+            self.assertEqual(pulledData, cheeseburgers)
+            d.dirExists('/mnt/sdcard')
+
+    def test_pull_failure(self):
+
+        # this test simulates only receiving a few bytes of what we expect
+        # to be larger file
+        remoteName = "/mnt/sdcard/cheeseburgers"
+        a = MockAgent(self, commands = [("pull %s" % remoteName,
+                                         "%s,15\n%s" % (remoteName,
+                                                        "cheeseburgh"))])
+        d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        exceptionThrown = False
+        try:
+            d.pullFile("/mnt/sdcard/cheeseburgers")
+        except mozdevice.DMError:
+            exceptionThrown = True
+        self.assertTrue(exceptionThrown)
+
+if __name__ == '__main__':
+    unittest.main()
+
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_push.py
@@ -0,0 +1,91 @@
+from sut import MockAgent
+import mozdevice
+import unittest
+import hashlib
+import tempfile
+import os
+
+class PushTest(unittest.TestCase):
+
+    def test_push(self):
+        pushfile = "1234ABCD"
+        mdsum = hashlib.md5()
+        mdsum.update(pushfile)
+        expectedResponse = mdsum.hexdigest()
+
+        # (good response, no exception), (bad response, exception)
+        for response in [ (expectedResponse, False), ("BADHASH", True) ]:
+            cmd = "push /mnt/sdcard/foobar %s" % len(pushfile)
+            a = MockAgent(self, commands = [("isdir /mnt", "TRUE"),
+                                            ("isdir /mnt/sdcard", "TRUE"),
+                                            (cmd, response[0])])
+            exceptionThrown = False
+            with tempfile.NamedTemporaryFile() as f:
+                try:
+                    f.write(pushfile)
+                    f.flush()
+                    d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+                    d.pushFile(f.name, '/mnt/sdcard/foobar')
+                except mozdevice.DMError, e:
+                    exceptionThrown = True
+                self.assertEqual(exceptionThrown, response[1])
+            a.wait()
+
+    def test_push_dir(self):
+        pushfile = "1234ABCD"
+        mdsum = hashlib.md5()
+        mdsum.update(pushfile)
+        expectedFileResponse = mdsum.hexdigest()
+
+        tempdir = tempfile.mkdtemp()
+        complex_path = os.path.join(tempdir, "baz")
+        os.mkdir(complex_path)
+        f = tempfile.NamedTemporaryFile(dir=complex_path)
+        f.write(pushfile)
+        f.flush()
+
+        subTests = [ { 'cmds': [ ("isdir /mnt", "TRUE"),
+                                 ("isdir /mnt/sdcard", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "TRUE"),
+                                 ("isdir /mnt", "TRUE"),
+                                 ("isdir /mnt/sdcard", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "TRUE"),
+                                 ("push /mnt/sdcard//baz/%s %s" %
+                                  (os.path.basename(f.name), len(pushfile)),
+                                  expectedFileResponse) ],
+                       'expectException': False },
+                     { 'cmds': [ ("isdir /mnt", "TRUE"),
+                                 ("isdir /mnt/sdcard", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "TRUE"),
+                                 ("isdir /mnt", "TRUE"),
+                                 ("isdir /mnt/sdcard", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "TRUE"),
+                                 ("push /mnt/sdcard//baz/%s %s" %
+                                  (os.path.basename(f.name), len(pushfile)),
+                                  "BADHASH") ],
+                       'expectException': True },
+                     { 'cmds': [ ("isdir /mnt", "FALSE"),
+                                 ("mkdr /mnt",
+                                  "##AGENT-WARNING## Could not create the directory /mnt") ],
+                       'expectException': True },
+
+                     ]
+
+        for subTest in subTests:
+            a = MockAgent(self, commands = subTest['cmds'])
+
+            exceptionThrown = False
+            try:
+                mozdevice.DroidSUT.debug = 4
+                d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+                d.pushDir(tempdir, "/mnt/sdcard")
+            except mozdevice.DMError, e:
+                exceptionThrown = True
+            self.assertEqual(exceptionThrown, subTest['expectException'])
+
+            a.wait()
+
+        # FIXME: delete directory when done
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/mozhttpd/setup.py
+++ b/testing/mozbase/mozhttpd/setup.py
@@ -6,17 +6,17 @@ import os
 from setuptools import setup
 
 try:
     here = os.path.dirname(os.path.abspath(__file__))
     description = file(os.path.join(here, 'README.md')).read()
 except IOError:
     description = None
 
-PACKAGE_VERSION = '0.3'
+PACKAGE_VERSION = '0.4'
 
 deps = []
 
 setup(name='mozhttpd',
       version=PACKAGE_VERSION,
       description="basic python webserver, tested with talos",
       long_description=description,
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
--- a/testing/mozbase/mozinfo/mozinfo/mozinfo.py
+++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py
@@ -58,43 +58,46 @@ if system in ["Microsoft", "Windows"]:
         service_pack = os.sys.getwindowsversion()[4]
         info['service_pack'] = service_pack
 elif system == "Linux":
     (distro, version, codename) = platform.dist()
     version = "%s %s" % (distro, version)
     if not processor:
         processor = machine
     info['os'] = 'linux'
+elif system in ['DragonFly', 'FreeBSD', 'NetBSD', 'OpenBSD']:
+    info['os'] = 'bsd'
+    version = sys.platform
 elif system == "Darwin":
     (release, versioninfo, machine) = platform.mac_ver()
     version = "OS X %s" % release
     info['os'] = 'mac'
 elif sys.platform in ('solaris', 'sunos5'):
     info['os'] = 'unix'
     version = sys.platform
 info['version'] = version # os version
 
 # processor type and bits
 if processor in ["i386", "i686"]:
     if bits == "32bit":
         processor = "x86"
     elif bits == "64bit":
         processor = "x86_64"
-elif processor == "AMD64":
+elif processor.upper() == "AMD64":
     bits = "64bit"
     processor = "x86_64"
 elif processor == "Power Macintosh":
     processor = "ppc"
 bits = re.search('(\d+)bit', bits).group(1)
 info.update({'processor': processor,
              'bits': int(bits),
             })
 
 # standard value of choices, for easy inspection
-choices = {'os': ['linux', 'win', 'mac', 'unix'],
+choices = {'os': ['linux', 'bsd', 'win', 'mac', 'unix'],
            'bits': [32, 64],
            'processor': ['x86', 'x86_64', 'ppc']}
 
 
 def sanitize(info):
     """Do some sanitization of input values, primarily
     to handle universal Mac builds."""
     if "processor" in info and info["processor"] == "universal-x86-x86_64":
@@ -112,17 +115,17 @@ def update(new_info):
     info.update(new_info)
     sanitize(info)
     globals().update(info)
 
     # convenience data for os access
     for os_name in choices['os']:
         globals()['is' + os_name.title()] = info['os'] == os_name
     # unix is special
-    if isLinux:
+    if isLinux or isBsd:
         globals()['isUnix'] = True
 
 update({})
 
 # exports
 __all__ = info.keys()
 __all__ += ['is' + os_name.title() for os_name in choices['os']]
 __all__ += ['info', 'unknown', 'main', 'choices', 'update']
--- a/testing/mozbase/mozinfo/setup.py
+++ b/testing/mozbase/mozinfo/setup.py
@@ -1,17 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 import os
 from setuptools import setup
 
-PACKAGE_VERSION = '0.3.3'
+PACKAGE_VERSION = '0.4'
 
 # get documentation from the README
 try:
     here = os.path.dirname(os.path.abspath(__file__))
     description = file(os.path.join(here, 'README.md')).read()
 except (OSError, IOError):
     description = ''
 
--- a/testing/mozbase/mozinstall/mozinstall/mozinstall.py
+++ b/testing/mozbase/mozinstall/mozinstall/mozinstall.py
@@ -2,16 +2,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """Module to handle the installation and uninstallation of Gecko based
 applications across platforms.
 
 """
 import mozinfo
+import mozfile
 from optparse import OptionParser
 import os
 import shutil
 import subprocess
 import sys
 import tarfile
 import time
 import zipfile
@@ -107,17 +108,17 @@ def install(src, dest):
 
     if not os.path.exists(dest):
         os.makedirs(dest)
 
     trbk = None
     try:
         install_dir = None
         if zipfile.is_zipfile(src) or tarfile.is_tarfile(src):
-            install_dir = _extract(src, dest)[0]
+            install_dir = mozfile.extract(src, dest)[0]
         elif src.lower().endswith('.dmg'):
             install_dir = _install_dmg(src, dest)
         elif src.lower().endswith('.exe'):
             install_dir = _install_exe(src, dest)
 
         return install_dir
 
     except Exception, e:
@@ -202,70 +203,16 @@ def uninstall(install_folder):
                 # http://docs.python.org/library/sys.html#sys.exc_info
                 del trbk
 
     # Ensure that we remove any trace of the installation. Even the uninstaller
     # on Windows leaves files behind we have to explicitely remove.
     shutil.rmtree(install_folder)
 
 
-def _extract(src, dest):
-    """Extract a tar or zip file into the destination folder and return the
-    application folder.
-
-    Arguments:
-    src -- archive which has to be extracted
-    dest -- the path to extract to
-
-    """
-    if zipfile.is_zipfile(src):
-        bundle = zipfile.ZipFile(src)
-
-        # FIXME: replace with zip.extractall() when we require python 2.6
-        namelist = bundle.namelist()
-        for name in bundle.namelist():
-            filename = os.path.realpath(os.path.join(dest, name))
-            if name.endswith('/'):
-                os.makedirs(filename)
-            else:
-                path = os.path.dirname(filename)
-                if not os.path.isdir(path):
-                    os.makedirs(path)
-                _dest = open(filename, 'wb')
-                _dest.write(bundle.read(name))
-                _dest.close()
-
-    elif tarfile.is_tarfile(src):
-        bundle = tarfile.open(src)
-        namelist = bundle.getnames()
-
-        if hasattr(bundle, 'extractall'):
-            # tarfile.extractall doesn't exist in Python 2.4
-            bundle.extractall(path=dest)
-        else:
-            for name in namelist:
-                bundle.extract(name, path=dest)
-    else:
-        return
-
-    bundle.close()
-
-    # namelist returns paths with forward slashes even in windows
-    top_level_files = [os.path.join(dest, name) for name in namelist
-                             if len(name.rstrip('/').split('/')) == 1]
-
-    # namelist doesn't include folders, append these to the list
-    for name in namelist:
-        root = os.path.join(dest, name[:name.find('/')])
-        if root not in top_level_files:
-            top_level_files.append(root)
-
-    return top_level_files
-
-
 def _install_dmg(src, dest):
     """Extract a dmg file into the destination folder and return the
     application folder.
 
     Arguments:
     src -- DMG image which has to be extracted
     dest -- the path to extract to
 
--- a/testing/mozbase/mozinstall/setup.py
+++ b/testing/mozbase/mozinstall/setup.py
@@ -6,19 +6,21 @@ import os
 from setuptools import setup
 
 try:
     here = os.path.dirname(os.path.abspath(__file__))
     description = file(os.path.join(here, 'README.md')).read()
 except IOError:
     description = None
 
-PACKAGE_VERSION = '1.2'
+PACKAGE_VERSION = '1.4'
 
-deps = ['mozinfo==0.3.3']
+deps = ['mozinfo == 0.4',
+        'mozfile'
+       ]
 
 setup(name='mozInstall',
       version=PACKAGE_VERSION,
       description="This is a utility package for installing and uninstalling "
                   "Mozilla applications on various platforms.",
       long_description=description,
       # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       classifiers=['Environment :: Console',
--- a/testing/mozbase/mozlog/mozlog/logger.py
+++ b/testing/mozbase/mozlog/mozlog/logger.py
@@ -42,24 +42,28 @@ class _MozLogger(_LoggerClass):
     def testFail(self, message, *args, **kwargs):
         self.log(FAIL, message, *args, **kwargs)
 
     def testKnownFail(self, message, *args, **kwargs):
         self.log(KNOWN_FAIL, message, *args, **kwargs)
 
 class _MozFormatter(Formatter):
     """
-    MozFormatter class used for default formatting
-    This can easily be overriden with the log handler's setFormatter()
+    MozFormatter class used to standardize formatting
+    If a different format is desired, this can be explicitly
+    overriden with the log handler's setFormatter() method
     """
     level_length = 0
     max_level_length = len('TEST-START')
 
     def __init__(self):
-        pass
+        """
+        Formatter.__init__ has fmt and datefmt parameters that won't have
+        any affect on a MozFormatter instance. Bypass it to avoid confusion.
+        """
 
     def format(self, record):
         record.message = record.getMessage()
 
         # Handles padding so record levels align nicely
         if len(record.levelname) > self.level_length:
             pad = 0
             if len(record.levelname) <= self.max_level_length:
--- a/testing/mozbase/mozprocess/mozprocess/processhandler.py
+++ b/testing/mozbase/mozprocess/mozprocess/processhandler.py
@@ -307,17 +307,17 @@ falling back to not using job objects fo
                         diff = datetime.now() - countdowntokill
                         # Arbitrarily wait 3 minutes for windows to get its act together
                         # Windows sometimes takes a small nap between notifying the
                         # IO Completion port and actually killing the children, and we
                         # don't want to mistake that situation for the situation of an unexpected
                         # parent abort (which is what we're looking for here).
                         if diff.seconds > self.MAX_IOCOMPLETION_PORT_NOTIFICATION_DELAY:
                             print >> sys.stderr, "Parent process %s exited with children alive:" % self.pid
-                            print >> sys.stderr, "PIDS: %s" %  ', '.join(self._spawned_procs.keys())
+                            print >> sys.stderr, "PIDS: %s" %  ', '.join([str(i) for i in self._spawned_procs])
                             print >> sys.stderr, "Attempting to kill them..."
                             self.kill()
                             self._process_events.put({self.pid: 'FINISHED'})
 
                     if not portstatus:
                         # Check to see what happened
                         errcode = winprocess.GetLastError()
                         if errcode == winprocess.ERROR_ABANDONED_WAIT_0:
--- a/testing/mozbase/mozrunner/setup.py
+++ b/testing/mozbase/mozrunner/setup.py
@@ -2,27 +2,27 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import sys
 from setuptools import setup
 
 PACKAGE_NAME = "mozrunner"
-PACKAGE_VERSION = '5.12'
+PACKAGE_VERSION = '5.13'
 
 desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
 # take description from README
 here = os.path.dirname(os.path.abspath(__file__))
 try:
     description = file(os.path.join(here, 'README.md')).read()
 except (OSError, IOError):
     description = ''
 
-deps = ['mozinfo == 0.3.3',
+deps = ['mozinfo == 0.4',
         'mozprocess == 0.7',
         'mozprofile == 0.4',
        ]
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
 setup(name=PACKAGE_NAME,