Bug 895940 - Mirror mozbase to m-c, r=jhammel
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Thu, 25 Jul 2013 16:27:53 -0400
changeset 140058 4f7c7217108a5ea9e4bf67244ba45e7698abd46a
parent 140057 c8bc029b8553c0b5868d2ec15f8f38d6027aa447
child 140059 300f5b7d72e1d9306b251aba446557f34eec8057
push idunknown
push userunknown
push dateunknown
reviewersjhammel
bugs895940
milestone25.0a1
Bug 895940 - Mirror mozbase to m-c, r=jhammel
testing/mozbase/README.md
testing/mozbase/manifestdestiny/README.md
testing/mozbase/manifestdestiny/manifestparser/manifestparser.py
testing/mozbase/manifestdestiny/setup.py
testing/mozbase/manifestdestiny/tests/comment-example.ini
testing/mozbase/manifestdestiny/tests/manifest.ini
testing/mozbase/manifestdestiny/tests/test_manifestparser.py
testing/mozbase/manifestdestiny/tests/test_read_ini.py
testing/mozbase/manifestdestiny/tests/test_testmanifest.py
testing/mozbase/mozb2g/mozb2g/b2gmixin.py
testing/mozbase/mozb2g/setup.py
testing/mozbase/mozcrash/setup.py
testing/mozbase/mozcrash/tests/test.py
testing/mozbase/mozdevice/mozdevice/__init__.py
testing/mozbase/mozdevice/mozdevice/devicemanager.py
testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
testing/mozbase/mozdevice/setup.py
testing/mozbase/mozdevice/sut_tests/test_fileExists.py
testing/mozbase/mozdevice/tests/manifest.ini
testing/mozbase/mozdevice/tests/sut_app.py
testing/mozbase/mozdevice/tests/sut_chmod.py
testing/mozbase/mozdevice/tests/sut_fileExists.py
testing/mozbase/mozdevice/tests/sut_fileMethods.py
testing/mozbase/mozdevice/tests/sut_info.py
testing/mozbase/mozdevice/tests/sut_ip.py
testing/mozbase/mozdevice/tests/sut_kill.py
testing/mozbase/mozdevice/tests/sut_list.py
testing/mozbase/mozdevice/tests/sut_logcat.py
testing/mozbase/mozdevice/tests/sut_mkdir.py
testing/mozbase/mozdevice/tests/sut_push.py
testing/mozbase/mozdevice/tests/sut_remove.py
testing/mozbase/mozdevice/tests/sut_time.py
testing/mozbase/mozdevice/tests/sut_unpackfile.py
testing/mozbase/mozfile/mozfile/mozfile.py
testing/mozbase/mozfile/setup.py
testing/mozbase/mozfile/tests/manifest.ini
testing/mozbase/mozfile/tests/stubs.py
testing/mozbase/mozfile/tests/test.py
testing/mozbase/mozfile/tests/test_extract.py
testing/mozbase/mozfile/tests/test_load.py
testing/mozbase/mozfile/tests/test_rmtree.py
testing/mozbase/mozfile/tests/test_tempdir.py
testing/mozbase/mozfile/tests/test_tempfile.py
testing/mozbase/mozfile/tests/test_url.py
testing/mozbase/mozhttpd/README.md
testing/mozbase/mozhttpd/mozhttpd/__init__.py
testing/mozbase/mozhttpd/mozhttpd/iface.py
testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
testing/mozbase/mozhttpd/setup.py
testing/mozbase/mozhttpd/tests/baseurl.py
testing/mozbase/mozhttpd/tests/basic.py
testing/mozbase/mozhttpd/tests/manifest.ini
testing/mozbase/mozinfo/mozinfo/__init__.py
testing/mozbase/mozinfo/mozinfo/mozinfo.py
testing/mozbase/mozinfo/setup.py
testing/mozbase/mozinfo/tests/manifest.ini
testing/mozbase/mozinfo/tests/test.py
testing/mozbase/mozinstall/mozinstall/mozinstall.py
testing/mozbase/mozinstall/setup.py
testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.dmg
testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.exe
testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.tar.bz2
testing/mozbase/mozinstall/tests/Installer-Stubs/firefox.zip
testing/mozbase/mozinstall/tests/manifest.ini
testing/mozbase/mozinstall/tests/test.py
testing/mozbase/mozlog/README.md
testing/mozbase/mozlog/mozlog/__init__.py
testing/mozbase/mozlog/mozlog/logger.py
testing/mozbase/mozlog/mozlog/loglistener.py
testing/mozbase/mozlog/setup.py
testing/mozbase/mozlog/tests/manifest.ini
testing/mozbase/mozlog/tests/test_logger.py
testing/mozbase/moznetwork/moznetwork/__init__.py
testing/mozbase/moznetwork/moznetwork/moznetwork.py
testing/mozbase/moznetwork/setup.py
testing/mozbase/moznetwork/tests/manifest.ini
testing/mozbase/moznetwork/tests/test.py
testing/mozbase/mozprocess/setup.py
testing/mozbase/mozprocess/tests/Makefile
testing/mozbase/mozprofile/mozprofile/__init__.py
testing/mozbase/mozprofile/mozprofile/permissions.py
testing/mozbase/mozprofile/mozprofile/prefs.py
testing/mozbase/mozprofile/mozprofile/profile.py
testing/mozbase/mozprofile/setup.py
testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js
testing/mozbase/mozprofile/tests/test_preferences.py
testing/mozbase/mozrunner/mozrunner/__init__.py
testing/mozbase/mozrunner/mozrunner/local.py
testing/mozbase/mozrunner/mozrunner/remote.py
testing/mozbase/mozrunner/mozrunner/resources/metrotestharness.exe
testing/mozbase/mozrunner/mozrunner/runner.py
testing/mozbase/mozrunner/setup.py
testing/mozbase/moztest/README.md
testing/mozbase/moztest/setup.py
testing/mozbase/setup_development.py
testing/mozbase/test-manifest.ini
--- a/testing/mozbase/README.md
+++ b/testing/mozbase/README.md
@@ -7,13 +7,13 @@ Eideticker.
 Learn more about mozbase at the [project page][].
 
 Read [detailed docs][] online, or build them locally by running "make html" in
 the docs directory.
 
 Consult [open][] [bugs][] and feel free to file [new bugs][].
 
 
-[project page]: https://wiki.mozilla.org/Auto-tools/Projects/MozBase
+[project page]: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase
 [detailed docs]: http://mozbase.readthedocs.org/
 [open]: https://bugzilla.mozilla.org/buglist.cgi?resolution=---&component=Mozbase&product=Testing
 [bugs]: https://bugzilla.mozilla.org/buglist.cgi?resolution=---&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=mozbase
 [new bugs]: https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=Mozbase
deleted file mode 100644
--- a/testing/mozbase/manifestdestiny/README.md
+++ /dev/null
@@ -1,414 +0,0 @@
-Universal manifests for Mozilla test harnesses
-
-# What is ManifestDestiny?
-
-What ManifestDestiny gives you:
-
-* manifests are ordered lists of tests
-* tests may have an arbitrary number of key, value pairs
-* the parser returns an ordered list of test data structures, which
-  are just dicts with some keys.  For example, a test with no
-  user-specified metadata looks like this:
-
-    [{'expected': 'pass',
-      'path': '/home/mozilla/mozmill/src/ManifestDestiny/manifestdestiny/tests/testToolbar/testBackForwardButtons.js',
-      'relpath': 'testToolbar/testBackForwardButtons.js',
-      'name': 'testBackForwardButtons.js',
-      'here': '/home/mozilla/mozmill/src/ManifestDestiny/manifestdestiny/tests',
-      'manifest': '/home/mozilla/mozmill/src/ManifestDestiny/manifestdestiny/tests/manifest.ini',}]
-
-The keys displayed here (path, relpath, name, here, and manifest) are reserved keys for ManifestDestiny and any consuming APIs.  You can add additional key, value metadata to each test.
-
-
-# Why have test manifests?
-
-It is desirable to have a unified format for test manifests for testing
-[mozilla-central](http://hg.mozilla.org/mozilla-central), etc.
-
-* It is desirable to be able to selectively enable or disable tests based on platform or other conditions. This should be easy to do. Currently, since many of the harnesses just crawl directories, there is no effective way of disabling a test except for removal from mozilla-central
-* It is desriable to do this in a universal way so that enabling and disabling tests as well as other tasks are easily accessible to a wider audience than just those intimately familiar with the specific test framework.
-* It is desirable to have other metadata on top of the test. For instance, let's say a test is marked as skipped. It would be nice to give the reason why.
-
-
-Most Mozilla test harnesses work by crawling a directory structure.
-While this is straight-forward, manifests offer several practical
-advantages::
-
-* ability to turn a test off easily: if a test is broken on m-c
-  currently, the only way to turn it off, generally speaking, is just
-  removing the test.  Often this is undesirable, as if the test should
-  be dismissed because other people want to land and it can't be
-  investigated in real time (is it a failure? is the test bad? is no
-  one around that knows the test?), then backing out a test is at best
-  problematic.  With a manifest, a test may be disabled without
-  removing it from the tree and a bug filed with the appropriate
-  reason:
-
-     [test_broken.js]
-     disabled = https://bugzilla.mozilla.org/show_bug.cgi?id=123456
-
-* ability to run different (subsets of) tests on different
-  platforms. Traditionally, we've done a bit of magic or had the test
-  know what platform it would or would not run on. With manifests, you
-  can mark what platforms a test will or will not run on and change
-  these without changing the test.
-
-     [test_works_on_windows_only.js]
-     run-if = os == 'win'
-
-* ability to markup tests with metadata. We have a large, complicated,
-  and always changing infrastructure.  key, value metadata may be used
-  as an annotation to a test and appropriately curated and mined.  For
-  instance, we could mark certain tests as randomorange with a bug
-  number, if it were desirable.
-
-* ability to have sane and well-defined test-runs. You can keep
-  different manifests for different test runs and ``[include:]``
-  (sub)manifests as appropriate to your needs.
-
-
-# Manifest Format
-
-Manifests are .ini file with the section names denoting the path
-relative to the manifest:
-
-    [foo.js]
-    [bar.js]
-    [fleem.js]
-
-The sections are read in order. In addition, tests may include
-arbitrary key, value metadata to be used by the harness.  You may also
-have a `[DEFAULT]` section that will give key, value pairs that will
-be inherited by each test unless overridden:
-
-    [DEFAULT]
-    type = restart
-
-    [lilies.js]
-    color = white
-
-    [daffodils.js]
-    color = yellow
-    type = other
-    # override type from DEFAULT
-
-    [roses.js]
-    color = red
-
-You can also include other manifests:
-
-    [include:subdir/anothermanifest.ini]
-
-Manifests are included relative to the directory of the manifest with
-the `[include:]` directive unless they are absolute paths.
-
-
-# Data
-
-Manifest Destiny gives tests as a list of dictionaries (in python
-terms).
-
-* path: full path to the test
-* relpath: relative path starting from the root manifest location
-* name: file name of the test
-* here: the parent directory of the manifest
-* manifest: the path to the manifest containing the test
-
-This data corresponds to a one-line manifest:
-
-    [testToolbar/testBackForwardButtons.js]
-
-If additional key, values were specified, they would be in this dict
-as well.
-
-Outside of the reserved keys, the remaining key, values
-are up to convention to use.  There is a (currently very minimal)
-generic integration layer in ManifestDestiny for use of all harnesses,
-`manifestparser.TestManifest`.
-For instance, if the 'disabled' key is present, you can get the set of
-tests without disabled (various other queries are doable as well).
-
-Since the system is convention-based, the harnesses may do whatever
-they want with the data.  They may ignore it completely, they may use
-the provided integration layer, or they may provide their own
-integration layer.  This should allow whatever sort of logic is
-desired.  For instance, if in yourtestharness you wanted to run only on
-mondays for a certain class of tests:
-
-    tests = []
-    for test in manifests.tests:
-        if 'runOnDay' in test:
-           if calendar.day_name[calendar.weekday(*datetime.datetime.now().timetuple()[:3])].lower() == test['runOnDay'].lower():
-               tests.append(test)
-        else:
-           tests.append(test)
-
-To recap:
-* the manifests allow you to specify test data
-* the parser gives you this data
-* you can use it however you want or process it further as you need
-
-Tests are denoted by sections in an .ini file (see
-http://hg.mozilla.org/automation/ManifestDestiny/file/tip/manifestdestiny/tests/mozmill-example.ini).
-
-Additional manifest files may be included with an `[include:]` directive:
-
-    [include:path-to-additional-file.manifest]
-
-The path to included files is relative to the current manifest.
-
-The `[DEFAULT]` section contains variables that all tests inherit from.
-
-Included files will inherit the top-level variables but may override
-in their own `[DEFAULT]` section.
-
-
-# ManifestDestiny Architecture
-
-There is a two- or three-layered approach to the ManifestDestiny
-architecture, depending on your needs:
-
-1. ManifestParser: this is a generic parser for .ini manifests that
-facilitates the `[include:]` logic and the inheritence of
-metadata. Despite the internal variable being called `self.tests`
-(an oversight), this layer has nothing in particular to do with tests.
-
-2. TestManifest: this is a harness-agnostic integration layer that is
-test-specific. TestManifest faciliates `skip-if` and `run-if` logic.
-
-3. Optionally, a harness will have an integration layer than inherits
-from TestManifest if more harness-specific customization is desired at
-the manifest level.
-
-See the source code at https://github.com/mozilla/mozbase/tree/master/manifestdestiny
-and
-https://github.com/mozilla/mozbase/blob/master/manifestdestiny/manifestparser.py
-in particular.
-
-
-# Using Manifests
-
-A test harness will normally call `TestManifest.active_tests`:
-
-    def active_tests(self, exists=True, disabled=True, **tags):
-
-The manifests are passed to the `__init__` or `read` methods with
-appropriate arguments.  `active_tests` then allows you to select the
-tests you want:
-
-- exists : return only existing tests
-- disabled : whether to return disabled tests; if not these will be
-  filtered out; if True (the default), the `disabled` key of a
-  test's metadata will be present and will be set to the reason that a
-  test is disabled
-- tags : keys and values to filter on (e.g. `os='linux'`)
-
-`active_tests` looks for tests with `skip-if`
-`run-if`.  If the condition is or is not fulfilled,
-respectively, the test is marked as disabled.  For instance, if you
-pass `**dict(os='linux')` as `**tags`, if a test contains a line
-`skip-if = os == 'linux'` this test will be disabled, or
-`run-if = os = 'win'` in which case the test will also be disabled.  It
-is up to the harness to pass in tags appropriate to its usage.
-
-
-# Creating Manifests
-
-ManifestDestiny comes with a console script, `manifestparser create`, that
-may be used to create a seed manifest structure from a directory of
-files.  Run `manifestparser help create` for usage information.
-
-
-# Copying Manifests
-
-To copy tests and manifests from a source:
-
-    manifestparser [options] copy from_manifest to_directory -tag1 -tag2 --key1=value1 key2=value2 ...
-
-
-# Upating Tests
-
-To update the tests associated with with a manifest from a source
-directory:
-
-    manifestparser [options] update manifest from_directory -tag1 -tag2 --key1=value1 --key2=value2 ...
-
-
-# Usage example
-
-Here is an example of how to create manifests for a directory tree and
-update the tests listed in the manifests from an external source.
-
-## Creating Manifests
-
-Let's say you want to make a series of manifests for a given directory structure containing `.js` test files:
-
-    testing/mozmill/tests/firefox/
-    testing/mozmill/tests/firefox/testAwesomeBar/
-    testing/mozmill/tests/firefox/testPreferences/
-    testing/mozmill/tests/firefox/testPrivateBrowsing/
-    testing/mozmill/tests/firefox/testSessionStore/
-    testing/mozmill/tests/firefox/testTechnicalTools/
-    testing/mozmill/tests/firefox/testToolbar/
-    testing/mozmill/tests/firefox/restartTests
-
-You can use `manifestparser create` to do this:
-
-    $ manifestparser help create
-    Usage: manifestparser.py [options] create directory <directory> <...>
-
-         create a manifest from a list of directories
-
-    Options:
-      -p PATTERN, --pattern=PATTERN
-                            glob pattern for files
-      -i IGNORE, --ignore=IGNORE
-                            directories to ignore
-      -w IN_PLACE, --in-place=IN_PLACE
-                            Write .ini files in place; filename to write to
-
-We only want `.js` files and we want to skip the `restartTests` directory.
-We also want to write a manifest per directory, so I use the `--in-place`
-option to write the manifests:
-
-    manifestparser create . -i restartTests -p '*.js' -w manifest.ini
-
-This creates a manifest.ini per directory that we care about with the JS test files:
-
-    testing/mozmill/tests/firefox/manifest.ini
-    testing/mozmill/tests/firefox/testAwesomeBar/manifest.ini
-    testing/mozmill/tests/firefox/testPreferences/manifest.ini
-    testing/mozmill/tests/firefox/testPrivateBrowsing/manifest.ini
-    testing/mozmill/tests/firefox/testSessionStore/manifest.ini
-    testing/mozmill/tests/firefox/testTechnicalTools/manifest.ini
-    testing/mozmill/tests/firefox/testToolbar/manifest.ini
-
-The top-level `manifest.ini` merely has `[include:]` references to the sub manifests:
-
-    [include:testAwesomeBar/manifest.ini]
-    [include:testPreferences/manifest.ini]
-    [include:testPrivateBrowsing/manifest.ini]
-    [include:testSessionStore/manifest.ini]
-    [include:testTechnicalTools/manifest.ini]
-    [include:testToolbar/manifest.ini]
-
-Each sub-level manifest contains the (`.js`) test files relative to it.
-
-## Updating the tests from manifests
-
-You may need to update tests as given in manifests from a different source directory.
-`manifestparser update` was made for just this purpose:
-
-    Usage: manifestparser [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...
-
-        update the tests as listed in a manifest from a directory
-
-To update from a directory of tests in `~/mozmill/src/mozmill-tests/firefox/` run:
-
-    manifestparser update manifest.ini ~/mozmill/src/mozmill-tests/firefox/
-
-
-# Tests
-
-ManifestDestiny includes a suite of tests:
-
-https://github.com/mozilla/mozbase/tree/master/manifestdestiny/tests
-
-`test_manifest.txt` is a doctest that may be helpful in figuring out
-how to use the API.  Tests are run via `python test.py`.
-
-
-# Bugs
-
-Please file any bugs or feature requests at
-
-https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=ManifestParser
-
-Or contact jhammel @mozilla.org or in #ateam on irc.mozilla.org
-
-
-# CLI
-
-Run `manifestparser help` for usage information.
-
-To create a manifest from a set of directories:
-
-    manifestparser [options] create directory <directory> <...> [create-options]
-
-To output a manifest of tests:
-
-    manifestparser [options] write manifest <manifest> <...> -tag1 -tag2 --key1=value1 --key2=value2 ...
-
-To copy tests and manifests from a source:
-
-    manifestparser [options] copy from_manifest to_manifest -tag1 -tag2 --key1=value1 key2=value2 ...
-
-To update the tests associated with with a manifest from a source
-directory:
-
-    manifestparser [options] update manifest from_directory -tag1 -tag2 --key1=value1 --key2=value2 ...
-
-
-# Design Considerations
-
-Contrary to some opinion, manifestparser.py and the associated .ini
-format were not magically plucked from the sky but were descended upon
-through several design considerations.
-
-* test manifests should be ordered.  While python 2.6 and greater has
-  a ConfigParser that can use an ordered dictionary, it is a
-  requirement that we support python 2.4 for the build + testing
-  environment.  To that end, a `read_ini` function was implemented
-  in manifestparser.py that should be the equivalent of the .ini
-  dialect used by ConfigParser.
-
-* the manifest format should be easily human readable/writable.  While
-  there was initially some thought of using JSON, there was pushback
-  that JSON was not easily editable.  An ideal manifest format would
-  degenerate to a line-separated list of files.  While .ini format
-  requires an additional `[]` per line, and while there have been
-  complaints about this, hopefully this is good enough.
-
-* python does not have an in-built YAML parser.  Since it was
-  undesirable for manifestparser.py to have any dependencies, YAML was
-  dismissed as a format.
-
-* we could have used a proprietary format but decided against it.
-  Everyone knows .ini and there are good tools to deal with it.
-  However, since read_ini is the only function that transforms a
-  manifest to a list of key, value pairs, while the implications for
-  changing the format impacts downstream code, doing so should be
-  programmatically simple.
-
-* there should be a single file that may easily be
-  transported. Traditionally, test harnesses have lived in
-  mozilla-central. This is less true these days and it is increasingly
-  likely that more tests will not live in mozilla-central going
-  forward.  So `manifestparser.py` should be highly consumable. To
-  this end, it is a single file, as appropriate to mozilla-central,
-  which is also a working python package deployed to PyPI for easy
-  installation.
-
-
-# Developing ManifestDestiny
-
-ManifestDestiny is developed and maintained by Mozilla's
-[Automation and Testing Team](https://wiki.mozilla.org/Auto-tools).
-The project page is located at
-https://wiki.mozilla.org/Auto-tools/Projects/ManifestDestiny .
-
-
-# Historical Reference
-
-Date-ordered list of links about how manifests came to be where they are today::
-
-* https://wiki.mozilla.org/Auto-tools/Projects/UniversalManifest
-* http://alice.nodelman.net/blog/post/2010/05/
-* http://alice.nodelman.net/blog/post/universal-manifest-for-unit-tests-a-proposal/
-* https://elvis314.wordpress.com/2010/07/05/improving-personal-hygiene-by-adjusting-mochitests/
-* https://elvis314.wordpress.com/2010/07/27/types-of-data-we-care-about-in-a-manifest/
-* https://bugzilla.mozilla.org/show_bug.cgi?id=585106
-* http://elvis314.wordpress.com/2011/05/20/converting-xpcshell-from-listing-directories-to-a-manifest/
-* https://bugzilla.mozilla.org/show_bug.cgi?id=616999
-* https://wiki.mozilla.org/Auto-tools/Projects/ManifestDestiny
-* https://developer.mozilla.org/en/Writing_xpcshell-based_unit_tests#Adding_your_tests_to_the_xpcshell_manifest
--- a/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py
+++ b/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py
@@ -14,40 +14,17 @@ Mozilla universal manifest parser
 
 import os
 import re
 import shutil
 import sys
 from fnmatch import fnmatch
 from optparse import OptionParser
 
-# we need relpath, but it is introduced in python 2.6
-# http://docs.python.org/library/os.path.html
-try:
-    relpath = os.path.relpath
-except AttributeError:
-    def relpath(path, start):
-        """
-        Return a relative version of a path
-        from /usr/lib/python2.6/posixpath.py
-        """
-
-        if not path:
-            raise ValueError("no path specified")
-
-        start_list = os.path.abspath(start).split(os.path.sep)
-        path_list = os.path.abspath(path).split(os.path.sep)
-
-        # Work out how much of the filepath is shared by start and path.
-        i = len(os.path.commonprefix([start_list, path_list]))
-
-        rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:]
-        if not rel_list:
-            return os.curdir
-        return os.path.join(*rel_list)
+relpath = os.path.relpath
 
 # expr.py
 # from:
 # http://k0s.org/mozilla/hg/expressionparser
 # http://hg.mozilla.org/users/tmielczarek_mozilla.com/expressionparser
 
 # Implements a top-down parser/evaluator for simple boolean expressions.
 # ideas taken from http://effbot.org/zone/simple-top-down-parsing.htm
--- a/testing/mozbase/manifestdestiny/setup.py
+++ b/testing/mozbase/manifestdestiny/setup.py
@@ -1,35 +1,26 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
-import sys
-import os
-
-here = os.path.dirname(os.path.abspath(__file__))
-try:
-    filename = os.path.join(here, 'README.md')
-    description = file(filename).read()
-except:
-    description = ''
 
 PACKAGE_NAME = "ManifestDestiny"
-PACKAGE_VERSION = '0.5.6'
+PACKAGE_VERSION = '0.5.7'
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
-      description="Universal manifests for Mozilla test harnesses",
-      long_description=description,
+      description="Library to create and manage test manifests",
+      long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla manifests',
       author='Mozilla Automation and Testing Team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       zip_safe=False,
       packages=['manifestparser'],
       install_requires=[],
       entry_points="""
       [console_scripts]
       manifestparser = manifestparser.manifestparser:main
       """,
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/comment-example.ini
@@ -0,0 +1,11 @@
+; See https://bugzilla.mozilla.org/show_bug.cgi?id=813674
+
+[test_0180_fileInUse_xp_win_complete.js]
+[test_0181_fileInUse_xp_win_partial.js]
+[test_0182_rmrfdirFileInUse_xp_win_complete.js]
+[test_0183_rmrfdirFileInUse_xp_win_partial.js]
+[test_0184_fileInUse_xp_win_complete.js]
+[test_0185_fileInUse_xp_win_partial.js]
+[test_0186_rmrfdirFileInUse_xp_win_complete.js]
+[test_0187_rmrfdirFileInUse_xp_win_partial.js]
+; [test_0202_app_launch_apply_update_dirlocked.js] # Test disabled, bug 757632
\ No newline at end of file
--- a/testing/mozbase/manifestdestiny/tests/manifest.ini
+++ b/testing/mozbase/manifestdestiny/tests/manifest.ini
@@ -1,4 +1,5 @@
 # test manifest for mozbase tests
 [test_expressionparser.py]
 [test_manifestparser.py]
 [test_testmanifest.py]
+[test_read_ini.py]
--- a/testing/mozbase/manifestdestiny/tests/test_manifestparser.py
+++ b/testing/mozbase/manifestdestiny/tests/test_manifestparser.py
@@ -210,10 +210,21 @@ class TestManifestparser(unittest.TestCa
         """You can override the path in the section too.
         This shows that you can use a relative path"""
         path_example = os.path.join(here, 'path-example.ini')
         manifest = ManifestParser(manifests=(path_example,))
         self.assertEqual(manifest.tests[0]['path'],
                          os.path.join(here, 'fleem'))
 
 
+    def test_comments(self):
+        """
+        ensure comments work, see
+        https://bugzilla.mozilla.org/show_bug.cgi?id=813674
+        """
+        comment_example = os.path.join(here, 'comment-example.ini')
+        manifest = ManifestParser(manifests=(comment_example,))
+        self.assertEqual(len(manifest.tests), 8)
+        names = [i['name'] for i in manifest.tests]
+        self.assertFalse('test_0202_app_launch_apply_update_dirlocked.js' in names)
+
 if __name__ == '__main__':
     unittest.main()
new file mode 100755
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/test_read_ini.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+
+"""
+test .ini parsing
+
+ensure our .ini parser is doing what we want; to be deprecated for
+python's standard ConfigParser when 2.7 is reality so OrderedDict
+is the default:
+
+http://docs.python.org/2/library/configparser.html
+"""
+
+import unittest
+from manifestparser import read_ini
+from ConfigParser import ConfigParser
+from StringIO import StringIO
+
+class IniParserTest(unittest.TestCase):
+
+    def test_inline_comments(self):
+        """
+        We have no inline comments; so we're testing to ensure we don't:
+        https://bugzilla.mozilla.org/show_bug.cgi?id=855288
+        """
+
+        # test '#' inline comments (really, the lack thereof)
+        string = """[test_felinicity.py]
+kittens = true # This test requires kittens
+"""
+        buffer = StringIO()
+        buffer.write(string)
+        buffer.seek(0)
+        result = read_ini(buffer)[0][1]['kittens']
+        self.assertEqual(result, "true # This test requires kittens")
+
+        # compare this to ConfigParser
+        # python 2.7 ConfigParser does not support '#' as an
+        # inline comment delimeter (for "backwards compatability"):
+        # http://docs.python.org/2/library/configparser.html
+        buffer.seek(0)
+        parser = ConfigParser()
+        parser.readfp(buffer)
+        control = parser.get('test_felinicity.py', 'kittens')
+        self.assertEqual(result, control)
+
+        # test ';' inline comments (really, the lack thereof)
+        string = string.replace('#', ';')
+        buffer = StringIO()
+        buffer.write(string)
+        buffer.seek(0)
+        result = read_ini(buffer)[0][1]['kittens']
+        self.assertEqual(result, "true ; This test requires kittens")
+
+        # compare this to ConfigParser
+        # python 2.7 ConfigParser *does* support ';' as an
+        # inline comment delimeter (ibid).
+        # Python 3.x configparser, OTOH, does not support
+        # inline-comments by default.  It does support their specification,
+        # though they are weakly discouraged:
+        # http://docs.python.org/dev/library/configparser.html
+        buffer.seek(0)
+        parser = ConfigParser()
+        parser.readfp(buffer)
+        control = parser.get('test_felinicity.py', 'kittens')
+        self.assertNotEqual(result, control)
+
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/manifestdestiny/tests/test_testmanifest.py
+++ b/testing/mozbase/manifestdestiny/tests/test_testmanifest.py
@@ -24,11 +24,22 @@ class TestTestManifest(unittest.TestCase
 
         # You should be able to expect failures:
         last_test = manifest.active_tests(exists=False, toolkit='gtk2')[-1]
         self.assertEqual(last_test['name'], 'linuxtest')
         self.assertEqual(last_test['expected'], 'pass')
         last_test = manifest.active_tests(exists=False, toolkit='cocoa')[-1]
         self.assertEqual(last_test['expected'], 'fail')
 
+    def test_comments(self):
+        """
+        ensure comments work, see
+        https://bugzilla.mozilla.org/show_bug.cgi?id=813674
+        """
+        comment_example = os.path.join(here, 'comment-example.ini')
+        manifest = TestManifest(manifests=(comment_example,))
+        self.assertEqual(len(manifest.tests), 8)
+        names = [i['name'] for i in manifest.tests]
+        self.assertFalse('test_0202_app_launch_apply_update_dirlocked.js' in names)
+
 
 if __name__ == '__main__':
     unittest.main()
--- a/testing/mozbase/mozb2g/mozb2g/b2gmixin.py
+++ b/testing/mozbase/mozb2g/mozb2g/b2gmixin.py
@@ -14,28 +14,40 @@ import subprocess
 from marionette import Marionette
 from mozdevice import DeviceManagerADB, DeviceManagerSUT, DMError
 
 class B2GMixin(object):
     profileDir = None
     userJS = "/data/local/user.js"
     marionette = None
 
-    def __init__(self, host=None, marionette_port=2828, **kwargs):
-        self.marionetteHost = host
-        self.marionettePort = marionette_port
+    def __init__(self, host=None, marionetteHost=None, marionettePort=2828,
+                 **kwargs):
+ 
+        # (allowing marionneteHost to be specified seems a bit
+        # counter-intuitive since we normally get it below from the ip
+        # address, however we currently need it to be able to connect
+        # via adb port forwarding and localhost)
+        if marionetteHost:
+            self.marionetteHost = marionetteHost
+        elif host:
+            self.marionetteHost = host
+        self.marionettePort = marionettePort
 
     def cleanup(self):
+        """
+        If a user profile was setup on the device, restore it to the original.
+        """
         if self.profileDir:
             self.restoreProfile()
 
     def waitForPort(self, timeout):
-        """
-        Wait for the marionette server to respond.
-        Timeout parameter is in seconds
+        """Waits for the marionette server to respond, until the timeout specified.
+
+	:param timeout: Timeout parameter in seconds.
         """
         print "waiting for port"
         starttime = datetime.datetime.now()
         while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
             try:
                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                 print "trying %s %s" % (self.marionettePort, self.marionetteHost)
                 sock.connect((self.marionetteHost, self.marionettePort))
@@ -45,113 +57,125 @@ class B2GMixin(object):
                     return True
             except socket.error:
                 pass
             except Exception as e:
                 raise DMError("Could not connect to marionette: %s" % e)
             time.sleep(1)
         raise DMError("Could not communicate with Marionette port")
 
-    def setupMarionette(self):
+    def setupMarionette(self, scriptTimeout=60000):
         """
-        Start a marionette session.
-        If no host is given, then this will get the ip
-        of the device, and set up networking if needed.
+        Starts a marionette session.
+        If no host was given at init, the ip of the device will be retrieved
+        and networking will be established.
         """
         if not self.marionetteHost:
             self.setupDHCP()
             self.marionetteHost = self.getIP()
         if not self.marionette:
             self.marionette = Marionette(self.marionetteHost, self.marionettePort)
         if not self.marionette.session:
             self.waitForPort(30)
             self.marionette.start_session()
 
+        self.marionette.set_script_timeout(scriptTimeout)
+
     def restartB2G(self):
         """
-        Restarts the b2g process on the device
+        Restarts the b2g process on the device.
         """
         #restart b2g so we start with a clean slate
         if self.marionette and self.marionette.session:
             self.marionette.delete_session()
         self.shellCheckOutput(['stop', 'b2g'])
         # Wait for a bit to make sure B2G has completely shut down.
         tries = 10
         while "b2g" in self.shellCheckOutput(['ps', 'b2g']) and tries > 0:
             tries -= 1
             time.sleep(1)
         if tries == 0:
             raise DMError("Could not kill b2g process")
         self.shellCheckOutput(['start', 'b2g'])
 
     def setupProfile(self, prefs=None):
-        """
-        Sets up the user profile on the device,
-        The 'prefs' is a string of user_prefs to add to the profile.
-        If it is not set, it will default to a standard b2g testing profile.
+        """Sets up the user profile on the device.
+
+        :param prefs: String of user_prefs to add to the profile. Defaults to a standard b2g testing profile.
         """
+        # currently we have no custom prefs to set (when bug 800138 is fixed,
+        # we will probably want to enable marionette on an external ip by
+        # default)
         if not prefs:
-            prefs = """
-user_pref("power.screen.timeout", 999999);
-user_pref("devtools.debugger.force-local", false);
-            """
+            prefs = ""
+
         #remove previous user.js if there is one
         if not self.profileDir:
             self.profileDir = tempfile.mkdtemp()
         our_userJS = os.path.join(self.profileDir, "user.js")
         if os.path.exists(our_userJS):
             os.remove(our_userJS)
         #copy profile
         try:
-            output = self.getFile(self.userJS, our_userJS)
+            self.getFile(self.userJS, our_userJS)
         except subprocess.CalledProcessError:
             pass
         #if we successfully copied the profile, make a backup of the file
         if os.path.exists(our_userJS):
             self.shellCheckOutput(['dd', 'if=%s' % self.userJS, 'of=%s.orig' % self.userJS])
         with open(our_userJS, 'a') as user_file:
             user_file.write("%s" % prefs)
+
         self.pushFile(our_userJS, self.userJS)
         self.restartB2G()
         self.setupMarionette()
 
-    def setupDHCP(self, conn_type='eth0'):
-        """
-        Sets up networking.
+    def setupDHCP(self, interfaces=['eth0', 'wlan0']):
+        """Sets up networking.
 
-        If conn_type is not set, it will assume eth0.
+        :param interfaces: Network connection types to try. Defaults to eth0 and wlan0.
         """
+        all_interfaces = [line.split()[0] for line in \
+                          self.shellCheckOutput(['netcfg']).splitlines()[1:]]
+        interfaces_to_try = filter(lambda i: i in interfaces, all_interfaces)
+
         tries = 5
+        print "Setting up DHCP..."
         while tries > 0:
             print "attempts left: %d" % tries
             try:
-                self.shellCheckOutput(['netcfg', conn_type, 'dhcp'], timeout=10)
-                if self.getIP():
-                    return
+                for interface in interfaces_to_try:
+                    self.shellCheckOutput(['netcfg', interface, 'dhcp'],
+                                          timeout=10)
+                    if self.getIP(interfaces=[interface]):
+                        return
             except DMError:
                 pass
-            tries = tries - 1
+            time.sleep(1)
+            tries -= 1
         raise DMError("Could not set up network connection")
 
     def restoreProfile(self):
         """
-        Restores the original profile
+        Restores the original user profile on the device.
         """
         if not self.profileDir:
             raise DMError("There is no profile to restore")
         #if we successfully copied the profile, make a backup of the file
         our_userJS = os.path.join(self.profileDir, "user.js")
         if os.path.exists(our_userJS):
             self.shellCheckOutput(['dd', 'if=%s.orig' % self.userJS, 'of=%s' % self.userJS])
         shutil.rmtree(self.profileDir)
         self.profileDir = None
 
     def getAppInfo(self):
         """
         Returns the appinfo, with an additional "date" key.
+
+        :rtype: dictionary
         """
         if not self.marionette or not self.marionette.session:
             self.setupMarionette()
         self.marionette.set_context("chrome")
         appinfo = self.marionette.execute_script("""
                                 var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
                                 .getService(Components.interfaces.nsIXULAppInfo);
                                 return appInfo;
--- a/testing/mozbase/mozb2g/setup.py
+++ b/testing/mozbase/mozb2g/setup.py
@@ -1,33 +1,25 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
-import os
 from setuptools import setup
 
-PACKAGE_VERSION = '0.1'
+PACKAGE_VERSION = '0.3'
 
-# 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 = ['mozdevice', 'marionette_client']
+deps = ['mozdevice >= 0.16', 'marionette_client >= 0.5.2']
 
 setup(name='mozb2g',
       version=PACKAGE_VERSION,
       description="B2G specific code for device automation",
-      long_description=description,
+      long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='',
       author='Mozilla Automation and Testing Team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['mozb2g'],
       include_package_data=True,
       zip_safe=False,
       install_requires=deps
       )
--- a/testing/mozbase/mozcrash/setup.py
+++ b/testing/mozbase/mozcrash/setup.py
@@ -1,27 +1,27 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
-PACKAGE_VERSION = '0.6'
+PACKAGE_VERSION = '0.8'
 
 # dependencies
 deps = ['mozfile >= 0.3',
         'mozlog']
 
 setup(name='mozcrash',
       version=PACKAGE_VERSION,
       description="Library for printing stack traces from minidumps left behind by crashed processes",
       long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['mozcrash'],
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       )
--- a/testing/mozbase/mozcrash/tests/test.py
+++ b/testing/mozbase/mozcrash/tests/test.py
@@ -3,17 +3,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, unittest, subprocess, tempfile, shutil, urlparse, zipfile, StringIO
 import mozcrash, mozlog, mozhttpd
 
 # Make logs go away
-log = mozlog.getLogger("mozcrash", os.devnull)
+log = mozlog.getLogger("mozcrash", handler=mozlog.FileHandler(os.devnull))
 
 def popen_factory(stdouts):
     """
     Generate a class that can mock subprocess.Popen. |stdouts| is an iterable that
     should return an iterable for the stdout of each process in turn.
     """
     class mock_popen(object):
         def __init__(self, args, *args_rest, **kwargs):
--- a/testing/mozbase/mozdevice/mozdevice/__init__.py
+++ b/testing/mozbase/mozdevice/mozdevice/__init__.py
@@ -1,8 +1,8 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
-from devicemanager import DeviceManager, DMError
+from devicemanager import DeviceManager, DMError, ZeroconfListener
 from devicemanagerADB import DeviceManagerADB
 from devicemanagerSUT import DeviceManagerSUT
 from droid import DroidADB, DroidSUT, DroidConnectByHWID
--- a/testing/mozbase/mozdevice/mozdevice/devicemanager.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
@@ -1,16 +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 hashlib
 import mozlog
 import socket
 import os
+import posixpath
 import re
 import struct
 import StringIO
 import zlib
 
 from Zeroconf import Zeroconf, ServiceBrowser
 from functools import wraps
 
@@ -233,37 +234,37 @@ class DeviceManager(object):
 
     def mkDirs(self, filename):
         """
         Make directory structure on the device.
 
         WARNING: does not create last part of the path. For example, if asked to
         create `/mnt/sdcard/foo/bar/baz`, it will only create `/mnt/sdcard/foo/bar`
         """
-        dirParts = filename.rsplit('/', 1)
-        if not self.dirExists(dirParts[0]):
+        filename = posixpath.normpath(filename)
+        containing = posixpath.dirname(filename)
+        if not self.dirExists(containing):
             parts = filename.split('/')
-            name = ""
-            for part in parts:
-                if part is parts[-1]:
-                    break
+            name = "/"
+            for part in parts[:-1]:
                 if part != "":
-                    name += '/' + part
+                    name = posixpath.join(name, part)
                     self.mkDir(name) # mkDir will check previous existence
 
     @abstractmethod
     def dirExists(self, dirpath):
         """
         Returns whether dirpath exists and is a directory on the device file system.
         """
 
     @abstractmethod
     def fileExists(self, filepath):
         """
-        Return whether filepath exists and is a file on the device file system.
+        Return whether filepath exists on the device file system,
+        regardless of file type.
         """
 
     @abstractmethod
     def listFiles(self, rootdir):
         """
         Lists files on the device rootdir.
 
         Returns array of filenames, ['file1', 'file2', ...]
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
@@ -151,16 +151,45 @@ class DeviceManagerADB(DeviceManager):
             if m:
                 return_code = m.group(1)
                 outputfile.seek(-2, 2)
                 outputfile.truncate() # truncate off the return code
                 return int(return_code)
 
         return None
 
+    def forward(self, local, remote):
+        """
+        Forward socket connections.
+        
+        Forward specs are one of:
+          tcp:<port>
+          localabstract:<unix domain socket name>
+          localreserved:<unix domain socket name>
+          localfilesystem:<unix domain socket name>
+          dev:<character device name>
+          jdwp:<process pid> (remote only)
+        """
+        return self._checkCmd(['forward', local, remote])
+
+    def remount(self):
+        "Remounts the /system partition on the device read-write."
+        return self._checkCmd(['remount'])
+
+    def devices(self):
+        "Return a list of connected devices as (serial, status) tuples."
+        proc = self._runCmd(['devices'])
+        proc.stdout.readline() # ignore first line of output
+        devices = []
+        for line in iter(proc.stdout.readline, ''):
+            result = re.match('(.*?)\t(.*)', line)
+            if result:
+                devices.append((result.group(1), result.group(2)))
+        return devices
+
     def _connectRemoteADB(self):
         self._checkCmd(["connect", self.host + ":" + str(self.port)])
 
     def _disconnectRemoteADB(self):
         self._checkCmd(["disconnect", self.host + ":" + str(self.port)])
 
     def pushFile(self, localname, destname, retryLimit=None):
         # you might expect us to put the file *in* the directory in this case,
@@ -396,17 +425,17 @@ class DeviceManagerADB(DeviceManager):
         # ADB pull does not support offset and length, but we can instead
         # read only the requested portion of the local file
         if offset is not None and length is not None:
             f.seek(offset)
             ret = f.read(length)
         elif offset is not None:
             f.seek(offset)
             ret = f.read()
-        else: 
+        else:
             ret = f.read()
 
         f.close()
         os.remove(localFile)
         return ret
 
     def getFile(self, remoteFile, localFile):
         self._runPull(remoteFile, localFile)
@@ -487,31 +516,29 @@ class DeviceManagerADB(DeviceManager):
             return '/data/data/' + packageName
         elif (self._packageName and self.dirExists('/data/data/' + self._packageName)):
             return '/data/data/' + self._packageName
 
         # Failure (either not installed or not a recognized platform)
         raise DMError("Failed to get application root for: %s" % packageName)
 
     def reboot(self, wait = False, **kwargs):
-        self._runCmd(["reboot"])
-        if (not wait):
+        self._checkCmd(["reboot"])
+        if not wait:
             return
-        countdown = 40
-        while (countdown > 0):
-            self._checkCmd(["wait-for-device", "shell", "ls", "/sbin"])
+        self._checkCmd(["wait-for-device", "shell", "ls", "/sbin"])
 
     def updateApp(self, appBundlePath, **kwargs):
         return self._runCmd(["install", "-r", appBundlePath]).stdout.read()
 
     def getCurrentTime(self):
         timestr = self._runCmd(["shell", "date", "+%s"]).stdout.read().strip()
         if (not timestr or not timestr.isdigit()):
             raise DMError("Unable to get current time using date (got: '%s')" % timestr)
-        return str(int(timestr)*1000)
+        return int(timestr)*1000
 
     def getInfo(self, directive=None):
         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"):
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
@@ -363,25 +363,23 @@ class DeviceManagerSUT(DeviceManager):
             self._runCmds([{ 'cmd': 'mkdr ' + name }])
 
     def pushDir(self, localDir, remoteDir, retryLimit = None):
         retryLimit = retryLimit or self.retryLimit
         self._logger.info("pushing directory: %s to %s" % (localDir, remoteDir))
 
         existentDirectories = []
         for root, dirs, files in os.walk(localDir, followlinks=True):
-            parts = root.split(localDir)
+            _, subpath = root.split(localDir)
+            subpath = subpath.lstrip('/')
+            remoteRoot = posixpath.join(remoteDir, subpath)
             for f in files:
-                remoteRoot = remoteDir + '/' + parts[1]
-                if (remoteRoot.endswith('/')):
-                    remoteName = remoteRoot + f
-                else:
-                    remoteName = remoteRoot + '/' + f
+                remoteName = posixpath.join(remoteRoot, f)
 
-                if (parts[1] == ""):
+                if subpath == "":
                     remoteRoot = remoteDir
 
                 parent = os.path.dirname(remoteName)
                 if parent not in existentDirectories:
                     self.mkDirs(remoteName)
                     existentDirectories.append(parent)
 
                 self.pushFile(os.path.join(root, f), remoteName, retryLimit=retryLimit)
@@ -392,29 +390,34 @@ class DeviceManagerSUT(DeviceManager):
         if not ret:
             raise DMError('Automation Error: DeviceManager isdir returned null')
 
         return ret == 'TRUE'
 
     def fileExists(self, filepath):
         # Because we always have / style paths we make this a lot easier with some
         # assumptions
-        s = filepath.split('/')
-        containingpath = '/'.join(s[:-1])
-        return s[-1] in self.listFiles(containingpath)
+        filepath = posixpath.normpath(filepath)
+        # / should always exist but we can use this to check for things like
+        # having access to the filesystem
+        if filepath == '/':
+            return self.dirExists(filepath)
+        (containingpath, filename) = posixpath.split(filepath)
+        return filename in self.listFiles(containingpath)
 
     def listFiles(self, rootdir):
-        rootdir = rootdir.rstrip('/')
-        if (self.dirExists(rootdir) == False):
+        rootdir = posixpath.normpath(rootdir)
+        if not self.dirExists(rootdir):
             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>"
+            # special case on the agent: empty directories return just the
+            # string "<empty>"
             return []
         return files
 
     def removeFile(self, filename):
         self._logger.info("removing file: " + filename)
         if self.fileExists(filename):
             self._runCmds([{ 'cmd': 'rm ' + filename }])
 
@@ -869,17 +872,17 @@ class DeviceManagerSUT(DeviceManager):
         status = self._runCmds([{'cmd': cmd}])
 
         if ipAddr is not None:
             status = self._wait_for_reboot(ip, port)
 
         self._logger.debug("updateApp: got status back: %s" % status)
 
     def getCurrentTime(self):
-        return self._runCmds([{ 'cmd': 'clok' }]).strip()
+        return int(self._runCmds([{ 'cmd': 'clok' }]).strip())
 
     def _getCallbackIpAndPort(self, aIp, aPort):
         """
         Connect the ipaddress and port for a callback ping.
 
         Defaults to current IP address and ports starting at 30000.
         NOTE: the detection for current IP address only works on Linux!
         """
--- a/testing/mozbase/mozdevice/setup.py
+++ b/testing/mozbase/mozdevice/setup.py
@@ -1,15 +1,15 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
-PACKAGE_VERSION = '0.27'
+PACKAGE_VERSION = '0.28'
 
 setup(name='mozdevice',
       version=PACKAGE_VERSION,
       description="Mozilla-authored device management",
       long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='',
       author='Mozilla Automation and Testing Team',
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_fileExists.py
@@ -0,0 +1,37 @@
+# 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 tempfile
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+
+class FileExistsTestCase(DeviceManagerTestCase):
+    """This tests the "fileExists" command.
+    """
+
+    def testOnRoot(self):
+        self.assertTrue(self.dm.fileExists('/'))
+
+    def testOnNonexistent(self):
+        self.assertFalse(self.dm.fileExists('/doesNotExist'))
+
+    def testOnRegularFile(self):
+        remote_path = posixpath.join(self.dm.getDeviceRoot(), 'testFile')
+        self.assertFalse(self.dm.fileExists(remote_path))
+        with tempfile.NamedTemporaryFile() as f:
+            self.dm.pushFile(f.name, remote_path)
+        self.assertTrue(self.dm.fileExists(remote_path))
+        self.dm.removeFile(remote_path)
+
+    def testOnDirectory(self):
+        remote_path = posixpath.join(self.dm.getDeviceRoot(), 'testDir')
+        remote_path_file = posixpath.join(remote_path, 'testFile')
+        self.assertFalse(self.dm.fileExists(remote_path))
+        with tempfile.NamedTemporaryFile() as f:
+            self.dm.pushFile(f.name, remote_path_file)
+        self.assertTrue(self.dm.fileExists(remote_path))
+        self.dm.removeFile(remote_path_file)
+        self.dm.removeDir(remote_path)
+
--- a/testing/mozbase/mozdevice/tests/manifest.ini
+++ b/testing/mozbase/mozdevice/tests/manifest.ini
@@ -1,9 +1,21 @@
 [DEFAULT]
 skip-if = os == 'win'
 
+[sut_app.py]
 [sut_basic.py]
+[sut_chmod.py]
+[sut_fileExists.py]
+[sut_fileMethods.py]
+[sut_info.py]
+[sut_ip.py]
+[sut_kill.py]
+[sut_list.py]
+[sut_logcat.py]
 [sut_mkdir.py]
+[sut_ps.py]
 [sut_push.py]
 [sut_pull.py]
-[sut_ps.py]
-[droidsut_launch.py]
\ No newline at end of file
+[sut_remove.py]
+[sut_time.py]
+[sut_unpackfile.py]
+[droidsut_launch.py]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_app.py
@@ -0,0 +1,20 @@
+#/usr/bin/env python
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestApp(unittest.TestCase):
+
+    def test_getAppRoot(self):
+        command = [("getapproot org.mozilla.firefox",
+                    "/data/data/org.mozilla.firefox")]
+
+        m = MockAgent(self, commands=command)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+
+        self.assertEqual(command[0][1], d.getAppRoot('org.mozilla.firefox'))
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_chmod.py
@@ -0,0 +1,21 @@
+#/usr/bin/env python
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestChmod(unittest.TestCase):
+
+    def test_chmod(self):
+
+        command = [('chmod /mnt/sdcard/test', 'Changing permissions for /storage/emulated/legacy/Test\n'
+                                              '        <empty>\n'
+                                              'chmod /storage/emulated/legacy/Test ok\n')]
+        m = MockAgent(self, commands=command)
+        d = mozdevice.DroidSUT('127.0.0.1', port=m.port, logLevel=mozlog.DEBUG)
+
+        self.assertEqual(None, d.chmodDir('/mnt/sdcard/test'))
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_fileExists.py
@@ -0,0 +1,29 @@
+from sut import MockAgent
+import mozdevice
+import unittest
+
+class FileExistsTest(unittest.TestCase):
+
+    commands = [('isdir /', 'TRUE'),
+                ('cd /', ''),
+                ('ls', 'init')]
+
+    def test_onRoot(self):
+        root_commands = [('isdir /', 'TRUE')]
+        a = MockAgent(self, commands=root_commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        self.assertTrue(d.fileExists('/'))
+
+    def test_onNonexistent(self):
+        a = MockAgent(self, commands=self.commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        self.assertFalse(d.fileExists('/doesNotExist'))
+
+    def test_onRegularFile(self):
+        a = MockAgent(self, commands=self.commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+        self.assertTrue(d.fileExists('/init'))
+
+if __name__ == '__main__':
+    unittest.main()
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_fileMethods.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+
+import hashlib
+import mock
+import mozdevice
+import mozlog
+import shutil
+import tempfile
+import os
+import unittest
+from sut import MockAgent
+
+
+class TestFileMethods(unittest.TestCase):
+    """ Class to test misc file methods """
+
+    content = "What is the answer to the life, universe and everything? 42"
+    h = hashlib.md5()
+    h.update(content)
+    temp_hash = h.hexdigest()
+
+    def test_validateFile(self):
+
+        with tempfile.NamedTemporaryFile() as f:
+            f.write(self.content)
+            f.flush()
+
+            # Test Valid Hashes
+            commands_valid = [("hash /sdcard/test/file", self.temp_hash)]
+
+            m = MockAgent(self, commands=commands_valid)
+            d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+            self.assertTrue(d.validateFile('/sdcard/test/file', f.name))
+
+            # Test invalid hashes
+            commands_invalid = [("hash /sdcard/test/file", "0this0hash0is0completely0invalid")]
+
+            m = MockAgent(self, commands=commands_invalid)
+            d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+            self.assertFalse(d.validateFile('/sdcard/test/file', f.name))
+
+    def test_getFile(self):
+
+        fname = "/mnt/sdcard/file"
+        commands = [("pull %s" % fname, "%s,%s\n%s" % (fname, len(self.content), self.content)),
+                    ("hash %s" % fname, self.temp_hash)]
+
+        with tempfile.NamedTemporaryFile() as f:
+            m = MockAgent(self, commands=commands)
+            d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+            # No error means success
+            self.assertEqual(None, d.getFile(fname, f.name))
+
+    def test_getDirectory(self):
+
+        fname = "/mnt/sdcard/file"
+        commands = [("isdir /mnt/sdcard", "TRUE"),
+                    ("isdir /mnt/sdcard", "TRUE"),
+                    ("cd /mnt/sdcard", ""),
+                    ("ls", "file"),
+                    ("isdir %s" % fname, "FALSE"),
+                    ("pull %s" % fname, "%s,%s\n%s" % (fname, len(self.content), self.content)),
+                    ("hash %s" % fname, self.temp_hash)]
+
+        tmpdir = tempfile.mkdtemp()
+        m = MockAgent(self, commands=commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        self.assertEqual(None, d.getDirectory("/mnt/sdcard", tmpdir))
+
+        # Cleanup
+        shutil.rmtree(tmpdir)
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_info.py
@@ -0,0 +1,49 @@
+#/usr/bin/env python
+import mozdevice
+import mozlog
+import re
+import unittest
+from sut import MockAgent
+
+
+class TestGetInfo(unittest.TestCase):
+
+    commands = {'os': ('info os', 'JDQ39'),
+                'id': ('info id', '11:22:33:44:55:66'),
+                'uptime': ('info uptime', '0 days 0 hours 7 minutes 0 seconds 0 ms'),
+                'uptimemillis': ('info uptimemillis', '666'),
+                'systime': ('info systime', '2013/04/2 12:42:00:007'),
+                'screen': ('info screen', 'X:768 Y:1184'),
+                'rotation': ('info rotation', 'ROTATION:0'),
+                'memory': ('info memory', 'PA:1351032832, FREE: 878645248'),
+                'process': ('info process', '1000    527     system\n'
+                            '10091   3443    org.mozilla.firefox\n'
+                            '10112   3137    com.mozilla.SUTAgentAndroid\n'
+                            '10035   807     com.android.launcher'),
+                'disk': ('info disk', '/data: 6084923392 total, 980922368 available\n'
+                         '/system: 867999744 total, 332333056 available\n'
+                         '/mnt/sdcard: 6084923392 total, 980922368 available'),
+                'power': ('info power', 'Power status:\n'
+                          '  AC power OFFLINE\n'
+                          '  Battery charge LOW DISCHARGING\n'
+                          '  Remaining charge:      20%\n'
+                          '  Battery Temperature:   25.2 (c)'),
+                'sutuserinfo': ('info sutuserinfo', 'User Serial:0'),
+                'temperature': ('info temperature', 'Temperature: unknown')
+                }
+
+    def test_getInfo(self):
+
+        for directive in self.commands.keys():
+            m = MockAgent(self, commands=[self.commands[directive]])
+            d = mozdevice.DroidSUT('127.0.0.1', port=m.port, logLevel=mozlog.DEBUG)
+
+            expected = re.sub(r'\ +', ' ', self.commands[directive][1]).split('\n')
+            # Account for slightly different return format for 'process'
+            if directive is 'process':
+                expected = [[x] for x in expected]
+
+            self.assertEqual(d.getInfo(directive=directive)[directive], expected)
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_ip.py
@@ -0,0 +1,37 @@
+#/usr/bin/env python
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestGetIP(unittest.TestCase):
+    """ class to test IP methods """
+
+    commands = [('exec ifconfig eth0', 'eth0: ip 192.168.0.1 '
+                 'mask 255.255.255.0 flags [up broadcast running multicast]\n'
+                 'return code [0]'),
+                ('exec ifconfig wlan0', 'wlan0: ip 10.1.39.126\n'
+                 'mask 255.255.0.0 flags [up broadcast running multicast]\n'
+                 'return code [0]'),
+                ('exec ifconfig fake0', '##AGENT-WARNING## [ifconfig] '
+                 'command with arg(s) = [fake0] is currently not implemented.')
+                ]
+
+    def test_getIP_eth0(self):
+        m = MockAgent(self, commands=[self.commands[0]])
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        self.assertEqual('192.168.0.1', d.getIP(interfaces=['eth0']))
+
+    def test_getIP_wlan0(self):
+        m = MockAgent(self, commands=[self.commands[1]])
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        self.assertEqual('10.1.39.126', d.getIP(interfaces=['wlan0']))
+
+    def test_getIP_error(self):
+        m = MockAgent(self, commands=[self.commands[2]])
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        self.assertRaises(mozdevice.DMError, d.getIP, interfaces=['fake0'])
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_kill.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestKill(unittest.TestCase):
+
+    def test_killprocess(self):
+        commands = [("ps", "1000    1486    com.android.settings\n"
+                           "10016   420 com.android.location.fused\n"
+                           "10023   335 com.android.systemui\n"),
+                    ("kill com.android.settings",
+                     "Successfully killed com.android.settings\n")]
+        m = MockAgent(self, commands=commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        # No error raised means success
+        self.assertEqual(None,  d.killProcess("com.android.settings"))
+
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_list.py
@@ -0,0 +1,22 @@
+#/usr/bin/env python
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestListFiles(unittest.TestCase):
+    commands = [("isdir /mnt/sdcard", "TRUE"),
+                ("cd /mnt/sdcard", ""),
+                ("ls", "Android\nMusic\nPodcasts\nRingtones\nAlarms\n"
+                       "Notifications\nPictures\nMovies\nDownload\nDCIM\n")]
+
+    def test_listFiles(self):
+        m = MockAgent(self, commands=self.commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+
+        expected = (self.commands[2][1].strip()).split("\n")
+        self.assertEqual(expected, d.listFiles("/mnt/sdcard"))
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_logcat.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestLogCat(unittest.TestCase):
+    """ Class to test methods assosiated with logcat """
+
+    def test_getLogcat(self):
+
+        logcat_output = ("07-17 00:51:10.377 I/SUTAgentAndroid( 2933): onCreate\n\r"
+        "07-17 00:51:10.457 D/dalvikvm( 2933): GC_CONCURRENT freed 351K, 17% free 2523K/3008K, paused 5ms+2ms, total 38ms\n\r"
+        "07-17 00:51:10.497 I/SUTAgentAndroid( 2933): Caught exception creating file in /data/local/tmp: open failed: EACCES (Permission denied)\n\r"
+        "07-17 00:51:10.507 E/SUTAgentAndroid( 2933): ERROR: Cannot access world writeable test root\n\r"
+        "07-17 00:51:10.547 D/GeckoHealthRec( 3253): Initializing profile cache.\n\r"
+        "07-17 00:51:10.607 D/GeckoHealthRec( 3253): Looking for /data/data/org.mozilla.fennec/files/mozilla/c09kfhne.default/times.json\n\r"
+        "07-17 00:51:10.637 D/GeckoHealthRec( 3253): Using times.json for profile creation time.\n\r"
+        "07-17 00:51:10.707 D/GeckoHealthRec( 3253): Incorporating environment: times.json profile creation = 1374026758604\n\r"
+        "07-17 00:51:10.507 D/GeckoHealthRec( 3253): Requested prefs.\n\r"
+        "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): \n\r"
+        "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Private Dirty Memory         3176 kb\n\r"
+        "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Proportional Set Size Memory 5679 kb\n\r"
+        "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Shared Dirty Memory          9216 kb\n\r"
+        "07-17 06:55:21.627 I/SUTAgentAndroid( 3876): 127.0.0.1 : execsu /system/bin/logcat -v time -d dalvikvm:I "
+        "ConnectivityService:S WifiMonitor:S WifiStateTracker:S wpa_supplicant:S NetworkStateTracker:S\n\r"
+        "07-17 06:55:21.827 I/dalvikvm-heap( 3876): Grow heap (frag case) to 3.019MB for 102496-byte allocation\n\r"
+        "return code [0]")
+
+        inp = ("execsu /system/bin/logcat -v time -d "
+        "dalvikvm:I ConnectivityService:S WifiMonitor:S "
+        "WifiStateTracker:S wpa_supplicant:S NetworkStateTracker:S")
+
+        commands = [(inp, logcat_output)]
+        m = MockAgent(self, commands=commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        self.assertEqual(logcat_output[:-17].split('\r'), d.getLogcat())
+
+    def test_recordLogcat(self):
+
+        commands = [("execsu /system/bin/logcat -c", "return code [0]")]
+
+        m = MockAgent(self, commands=commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        # No error raised means success
+        self.assertEqual(None, d.recordLogcat())
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/mozdevice/tests/sut_mkdir.py
+++ b/testing/mozbase/mozdevice/tests/sut_mkdir.py
@@ -54,11 +54,20 @@ class MkDirsTest(unittest.TestCase):
                 ('mkdr /mnt/sdcard/foo',
                  '/mnt/sdcard/foo successfully created')]
         a = MockAgent(self, commands=cmds)
         d = mozdevice.DroidSUT('127.0.0.1', port=a.port,
                                logLevel=mozlog.DEBUG)
         d.mkDirs('/mnt/sdcard/foo/foo')
         a.wait()
 
+    def test_mkdirs_on_root(self):
+        cmds = [('isdir /', 'TRUE')]
+        a = MockAgent(self, commands=cmds)
+        d = mozdevice.DroidSUT('127.0.0.1', port=a.port,
+                               logLevel=mozlog.DEBUG)
+        d.mkDirs('/foo')
+
+        a.wait()
+
 
 if __name__ == '__main__':
     unittest.main()
--- a/testing/mozbase/mozdevice/tests/sut_push.py
+++ b/testing/mozbase/mozdevice/tests/sut_push.py
@@ -39,31 +39,31 @@ class PushTest(unittest.TestCase):
 
         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/sdcard//baz", "TRUE"),
-                                 ("isdir /mnt/sdcard//baz", "TRUE"),
-                                 ("push /mnt/sdcard//baz/%s %s\r\n%s" %
+        subTests = [ { 'cmds': [ ("isdir /mnt/sdcard/baz", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "TRUE"),
+                                 ("push /mnt/sdcard/baz/%s %s\r\n%s" %
                                   (os.path.basename(f.name), len(pushfile),
                                    pushfile),
                                   expectedFileResponse) ],
                        'expectException': False },
-                     { 'cmds': [ ("isdir /mnt/sdcard//baz", "TRUE"),
-                                 ("isdir /mnt/sdcard//baz", "TRUE"),
-                                 ("push /mnt/sdcard//baz/%s %s\r\n%s" %
+                     { 'cmds': [ ("isdir /mnt/sdcard/baz", "TRUE"),
+                                 ("isdir /mnt/sdcard/baz", "TRUE"),
+                                 ("push /mnt/sdcard/baz/%s %s\r\n%s" %
                                   (os.path.basename(f.name), len(pushfile),
                                    pushfile),
                                   "BADHASH") ],
                        'expectException': True },
-                     { 'cmds': [ ("isdir /mnt/sdcard//baz", "FALSE"),
+                     { 'cmds': [ ("isdir /mnt/sdcard/baz", "FALSE"),
                                  ("isdir /mnt", "FALSE"),
                                  ("mkdr /mnt",
                                   "##AGENT-WARNING## Could not create the directory /mnt") ],
                        'expectException': True },
 
                      ]
 
         for subTest in subTests:
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_remove.py
@@ -0,0 +1,24 @@
+#/usr/bin/env python
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestRemove(unittest.TestCase):
+
+    def test_removeDir(self):
+        commands = [("isdir /mnt/sdcard/test", "TRUE"),
+                    ("rmdr /mnt/sdcard/test", "Deleting file(s) from "
+                                            "/storage/emulated/legacy/Moztest\n"
+                                            "        <empty>\n"
+                                            "Deleting directory "
+                                            "/storage/emulated/legacy/Moztest\n")]
+
+        m = MockAgent(self, commands=commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        # No error implies we're all good
+        self.assertEqual(None, d.removeDir("/mnt/sdcard/test"))
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_time.py
@@ -0,0 +1,18 @@
+#/usr/bin/env python
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestGetCurrentTime(unittest.TestCase):
+
+    def test_getCurrentTime(self):
+        command = [('clok', '1349980200')]
+
+        m = MockAgent(self, commands=command)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        self.assertEqual(d.getCurrentTime(), int(command[0][1]))
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_unpackfile.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import mozdevice
+import mozlog
+import unittest
+from sut import MockAgent
+
+
+class TestUnpack(unittest.TestCase):
+
+    def test_unpackFile(self):
+
+        commands = [("isdir /mnt/sdcard/tests", "TRUE"),
+                    ("unzp /data/test/sample.zip /data/test/",
+                     "Checksum:          653400271\n"
+                     "1 of 1 successfully extracted\n")]
+        m = MockAgent(self, commands=commands)
+        d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=mozlog.DEBUG)
+        # No error being thrown imples all is well
+        self.assertEqual(None, d.unpackFile("/data/test/sample.zip",
+                                            "/data/test/"))
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/mozfile/mozfile/mozfile.py
+++ b/testing/mozbase/mozfile/mozfile/mozfile.py
@@ -1,26 +1,29 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+from contextlib import contextmanager
 import os
+import shutil
 import tarfile
 import tempfile
 import urlparse
 import urllib2
 import zipfile
 
 __all__ = ['extract_tarball',
            'extract_zip',
            'extract',
            'is_url',
            'load',
            'rmtree',
-           'NamedTemporaryFile']
+           'NamedTemporaryFile',
+           'TemporaryDirectory']
 
 
 ### utilities for extracting archives
 
 def extract_tarball(src, dest):
     """extract a .tar file"""
 
     bundle = tarfile.open(src)
@@ -44,17 +47,18 @@ def extract_zip(src, dest):
             print "src: %s" % src
             raise
 
     namelist = bundle.namelist()
 
     for name in namelist:
         filename = os.path.realpath(os.path.join(dest, name))
         if name.endswith('/'):
-            os.makedirs(filename)
+            if not os.path.isdir(filename):
+                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()
         mode = bundle.getinfo(name).external_attr >> 16 & 0x1FF
@@ -84,24 +88,26 @@ def extract(src, dest=None):
         namelist = extract_zip(src, dest)
     elif tarfile.is_tarfile(src):
         namelist = extract_tarball(src, dest)
     else:
         raise Exception("mozfile.extract: no archive format found for '%s'" %
                         src)
 
     # namelist returns paths with forward slashes even in windows
-    top_level_files = [os.path.join(dest, name) for name in namelist
+    top_level_files = [os.path.join(dest, name.rstrip('/')) 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)
+        index = name.find('/')
+        if index != -1:
+            root = os.path.join(dest, name[:index])
+            if root not in top_level_files:
+                top_level_files.append(root)
 
     return top_level_files
 
 
 def rmtree(dir):
     """Removes the specified directory tree
 
     This is a replacement for shutil.rmtree that works better under
@@ -227,8 +233,24 @@ def load(resource):
     if resource.startswith('file://'):
         resource = resource[len('file://'):]
 
     if not is_url(resource):
         # if no scheme is given, it is a file path
         return file(resource)
 
     return urllib2.urlopen(resource)
+
+@contextmanager
+def TemporaryDirectory():
+    """
+    create a temporary directory using tempfile.mkdtemp, and then clean it up.
+
+    Example usage:
+    with TemporaryDirectory() as tmp:
+       open(os.path.join(tmp, "a_temp_file"), "w").write("data")
+
+    """
+    tempdir = tempfile.mkdtemp()
+    try:
+        yield tempdir
+    finally:
+        shutil.rmtree(tempdir)
--- a/testing/mozbase/mozfile/setup.py
+++ b/testing/mozbase/mozfile/setup.py
@@ -1,24 +1,24 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
-PACKAGE_VERSION = '0.7'
+PACKAGE_VERSION = '0.10'
 
 setup(name='mozfile',
       version=PACKAGE_VERSION,
       description="Library of file utilities for use in Mozilla testing",
       long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['mozfile'],
       include_package_data=True,
       zip_safe=False,
       install_requires=[],
       tests_require=['mozhttpd']
       )
--- a/testing/mozbase/mozfile/tests/manifest.ini
+++ b/testing/mozbase/mozfile/tests/manifest.ini
@@ -1,4 +1,6 @@
-[test.py]
+[test_extract.py]
 [test_load.py]
+[test_rmtree.py]
+[test_tempdir.py]
 [test_tempfile.py]
 [test_url.py]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozfile/tests/stubs.py
@@ -0,0 +1,34 @@
+import os
+import shutil
+import tempfile
+
+
+# stub file paths
+files = [('foo.txt',),
+         ('foo', 'bar.txt'),
+         ('foo', 'bar', 'fleem.txt'),
+         ('foobar', 'fleem.txt'),
+         ('bar.txt')]
+
+
+def create_stub():
+    """create a stub directory"""
+
+    tempdir = tempfile.mkdtemp()
+    try:
+        for path in files:
+            fullpath = os.path.join(tempdir, *path)
+            dirname = os.path.dirname(fullpath)
+            if not os.path.exists(dirname):
+                os.makedirs(dirname)
+            contents = path[-1]
+            f = file(fullpath, 'w')
+            f.write(contents)
+            f.close()
+        return tempdir
+    except Exception, e:
+        try:
+            shutil.rmtree(tempdir)
+        except:
+            pass
+        raise e
deleted file mode 100755
--- a/testing/mozbase/mozfile/tests/test.py
+++ /dev/null
@@ -1,190 +0,0 @@
-#!/usr/bin/env python
-
-"""
-tests for mozfile
-"""
-
-import mozfile
-import os
-import shutil
-import tarfile
-import tempfile
-import unittest
-import zipfile
-
-# stub file paths
-files = [('foo.txt',),
-         ('foo', 'bar.txt'),
-         ('foo', 'bar', 'fleem.txt'),
-         ('foobar', 'fleem.txt'),
-         ('bar.txt')]
-
-def create_stub():
-    """create a stub directory"""
-
-    tempdir = tempfile.mkdtemp()
-    try:
-        for path in files:
-            fullpath = os.path.join(tempdir, *path)
-            dirname = os.path.dirname(fullpath)
-            if not os.path.exists(dirname):
-                os.makedirs(dirname)
-            contents = path[-1]
-            f = file(fullpath, 'w')
-            f.write(contents)
-            f.close()
-        return tempdir
-    except Exception, e:
-        try:
-            shutil.rmtree(tempdir)
-        except:
-            pass
-        raise e
-
-
-class TestExtract(unittest.TestCase):
-    """test extracting archives"""
-
-    def ensure_directory_contents(self, directory):
-        """ensure the directory contents match"""
-        for f in files:
-            path = os.path.join(directory, *f)
-            exists = os.path.exists(path)
-            if not exists:
-                print "%s does not exist" % (os.path.join(f))
-            self.assertTrue(exists)
-            if exists:
-                contents = file(path).read().strip()
-                self.assertTrue(contents == f[-1])
-
-    def test_extract_zipfile(self):
-        """test extracting a zipfile"""
-        _zipfile = self.create_zip()
-        self.assertTrue(os.path.exists(_zipfile))
-        try:
-            dest = tempfile.mkdtemp()
-            try:
-                mozfile.extract_zip(_zipfile, dest)
-                self.ensure_directory_contents(dest)
-            finally:
-                shutil.rmtree(dest)
-        finally:
-            os.remove(_zipfile)
-
-    def test_extract_tarball(self):
-        """test extracting a tarball"""
-        tarball = self.create_tarball()
-        self.assertTrue(os.path.exists(tarball))
-        try:
-            dest = tempfile.mkdtemp()
-            try:
-                mozfile.extract_tarball(tarball, dest)
-                self.ensure_directory_contents(dest)
-            finally:
-                shutil.rmtree(dest)
-        finally:
-            os.remove(tarball)
-
-    def test_extract(self):
-        """test the generalized extract function"""
-
-        # test extracting a tarball
-        tarball = self.create_tarball()
-        self.assertTrue(os.path.exists(tarball))
-        try:
-            dest = tempfile.mkdtemp()
-            try:
-                mozfile.extract(tarball, dest)
-                self.ensure_directory_contents(dest)
-            finally:
-                shutil.rmtree(dest)
-        finally:
-            os.remove(tarball)
-
-        # test extracting a zipfile
-        _zipfile = self.create_zip()
-        self.assertTrue(os.path.exists(_zipfile))
-        try:
-            dest = tempfile.mkdtemp()
-            try:
-                mozfile.extract_zip(_zipfile, dest)
-                self.ensure_directory_contents(dest)
-            finally:
-                shutil.rmtree(dest)
-        finally:
-            os.remove(_zipfile)
-
-        # test extracting some non-archive; this should fail
-        fd, filename = tempfile.mkstemp()
-        os.write(fd, 'This is not a zipfile or tarball')
-        os.close(fd)
-        exception = None
-        try:
-            dest = tempfile.mkdtemp()
-            mozfile.extract(filename, dest)
-        except Exception, exception:
-            pass
-        finally:
-            os.remove(filename)
-            os.rmdir(dest)
-        self.assertTrue(isinstance(exception, Exception))
-
-    ### utility functions
-
-    def create_tarball(self):
-        """create a stub tarball for testing"""
-        tempdir = create_stub()
-        filename = tempfile.mktemp(suffix='.tar')
-        archive = tarfile.TarFile(filename, mode='w')
-        try:
-            for path in files:
-                archive.add(os.path.join(tempdir, *path), arcname=os.path.join(*path))
-        except:
-            os.remove(archive)
-            raise
-        finally:
-            shutil.rmtree(tempdir)
-        archive.close()
-        return filename
-
-    def create_zip(self):
-        """create a stub zipfile for testing"""
-
-        tempdir = create_stub()
-        filename = tempfile.mktemp(suffix='.zip')
-        archive = zipfile.ZipFile(filename, mode='w')
-        try:
-            for path in files:
-                archive.write(os.path.join(tempdir, *path), arcname=os.path.join(*path))
-        except:
-            os.remove(filename)
-            raise
-        finally:
-            shutil.rmtree(tempdir)
-        archive.close()
-        return filename
-
-class TestRemoveTree(unittest.TestCase):
-    """test our ability to remove a directory tree"""
-
-    def test_remove_directory(self):
-        tempdir = create_stub()
-        self.assertTrue(os.path.exists(tempdir))
-        self.assertTrue(os.path.isdir(tempdir))
-        try:
-            mozfile.rmtree(tempdir)
-        except:
-            shutil.rmtree(tempdir)
-            raise
-        self.assertFalse(os.path.exists(tempdir))
-
-class TestNamedTemporaryFile(unittest.TestCase):
-    """test our fix for NamedTemporaryFile"""
-
-    def test_named_temporary_file(self):
-        temp = mozfile.NamedTemporaryFile()
-        temp.write("A simple test")
-        del temp
-
-if __name__ == '__main__':
-    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozfile/tests/test_extract.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+
+import mozfile
+import os
+import shutil
+import tarfile
+import tempfile
+import stubs
+import unittest
+import zipfile
+
+
+class TestExtract(unittest.TestCase):
+    """test extracting archives"""
+
+    def ensure_directory_contents(self, directory):
+        """ensure the directory contents match"""
+        for f in stubs.files:
+            path = os.path.join(directory, *f)
+            exists = os.path.exists(path)
+            if not exists:
+                print "%s does not exist" % (os.path.join(f))
+            self.assertTrue(exists)
+            if exists:
+                contents = file(path).read().strip()
+                self.assertTrue(contents == f[-1])
+
+    def test_extract_zipfile(self):
+        """test extracting a zipfile"""
+        _zipfile = self.create_zip()
+        self.assertTrue(os.path.exists(_zipfile))
+        try:
+            dest = tempfile.mkdtemp()
+            try:
+                mozfile.extract_zip(_zipfile, dest)
+                self.ensure_directory_contents(dest)
+            finally:
+                shutil.rmtree(dest)
+        finally:
+            os.remove(_zipfile)
+
+    def test_extract_tarball(self):
+        """test extracting a tarball"""
+        tarball = self.create_tarball()
+        self.assertTrue(os.path.exists(tarball))
+        try:
+            dest = tempfile.mkdtemp()
+            try:
+                mozfile.extract_tarball(tarball, dest)
+                self.ensure_directory_contents(dest)
+            finally:
+                shutil.rmtree(dest)
+        finally:
+            os.remove(tarball)
+
+    def test_extract(self):
+        """test the generalized extract function"""
+
+        # test extracting a tarball
+        tarball = self.create_tarball()
+        self.assertTrue(os.path.exists(tarball))
+        try:
+            dest = tempfile.mkdtemp()
+            try:
+                mozfile.extract(tarball, dest)
+                self.ensure_directory_contents(dest)
+            finally:
+                shutil.rmtree(dest)
+        finally:
+            os.remove(tarball)
+
+        # test extracting a zipfile
+        _zipfile = self.create_zip()
+        self.assertTrue(os.path.exists(_zipfile))
+        try:
+            dest = tempfile.mkdtemp()
+            try:
+                mozfile.extract_zip(_zipfile, dest)
+                self.ensure_directory_contents(dest)
+            finally:
+                shutil.rmtree(dest)
+        finally:
+            os.remove(_zipfile)
+
+        # test extracting some non-archive; this should fail
+        fd, filename = tempfile.mkstemp()
+        os.write(fd, 'This is not a zipfile or tarball')
+        os.close(fd)
+        exception = None
+        try:
+            dest = tempfile.mkdtemp()
+            mozfile.extract(filename, dest)
+        except Exception, exception:
+            pass
+        finally:
+            os.remove(filename)
+            os.rmdir(dest)
+        self.assertTrue(isinstance(exception, Exception))
+
+    ### utility functions
+
+    def create_tarball(self):
+        """create a stub tarball for testing"""
+        tempdir = stubs.create_stub()
+        filename = tempfile.mktemp(suffix='.tar')
+        archive = tarfile.TarFile(filename, mode='w')
+        try:
+            for path in stubs.files:
+                archive.add(os.path.join(tempdir, *path), arcname=os.path.join(*path))
+        except:
+            os.remove(archive)
+            raise
+        finally:
+            shutil.rmtree(tempdir)
+        archive.close()
+        return filename
+
+    def create_zip(self):
+        """create a stub zipfile for testing"""
+
+        tempdir = stubs.create_stub()
+        filename = tempfile.mktemp(suffix='.zip')
+        archive = zipfile.ZipFile(filename, mode='w')
+        try:
+            for path in stubs.files:
+                archive.write(os.path.join(tempdir, *path), arcname=os.path.join(*path))
+        except:
+            os.remove(filename)
+            raise
+        finally:
+            shutil.rmtree(tempdir)
+        archive.close()
+        return filename
--- a/testing/mozbase/mozfile/tests/test_load.py
+++ b/testing/mozbase/mozfile/tests/test_load.py
@@ -5,43 +5,42 @@ tests for mozfile.load
 """
 
 import mozhttpd
 import os
 import tempfile
 import unittest
 from mozfile import load
 
+
 class TestLoad(unittest.TestCase):
     """test the load function"""
 
     def test_http(self):
         """test with mozhttpd and a http:// URL"""
 
         def example(request):
             """example request handler"""
             body = 'example'
             return (200, {'Content-type': 'text/plain',
                           'Content-length': len(body)
                           }, body)
 
         host = '127.0.0.1'
         httpd = mozhttpd.MozHttpd(host=host,
-                                  port=8888,
                                   urlhandlers=[{'method': 'GET',
                                                 'path': '.*',
                                                 'function': example}])
         try:
             httpd.start(block=False)
-            content = load('http://127.0.0.1:8888/foo').read()
+            content = load(httpd.get_url()).read()
             self.assertEqual(content, 'example')
         finally:
             httpd.stop()
 
-
     def test_file_path(self):
         """test loading from file path"""
         try:
             # create a temporary file
             tmp = tempfile.NamedTemporaryFile(delete=False)
             tmp.write('foo bar')
             tmp.close()
 
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozfile/tests/test_rmtree.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+
+import mozfile
+import mozinfo
+import os
+import shutil
+import tempfile
+import unittest
+import stubs
+
+
+class TestRemoveTree(unittest.TestCase):
+    """test our ability to remove a directory tree"""
+
+    def setUp(self):
+        # Generate a stub
+        self.tempdir = stubs.create_stub()
+
+    def tearDown(self):
+        # Cleanup the stub if it sill exists
+        if os.path.isdir(self.tempdir):
+            mozfile.rmtree(self.tempdir)
+
+    def test_remove_directory(self):
+        self.assertTrue(os.path.isdir(self.tempdir))
+        try:
+            mozfile.rmtree(self.tempdir)
+        except:
+            shutil.rmtree(self.tempdir)
+            raise
+        self.assertFalse(os.path.exists(self.tempdir))
+
+    def test_remove_directory_with_open_file(self):
+        """ Tests handling when removing a directory tree
+            which has a file in it is still open """
+        # Open a file in the generated stub
+        filepath = os.path.join(self.tempdir, *stubs.files[1])
+        f = file(filepath, 'w')
+        f.write('foo-bar')
+        # keep file open and then try removing the dir-tree
+        if mozinfo.isWin:
+            # On the Windows family WindowsError should be raised.
+            self.assertRaises(WindowsError, mozfile.rmtree, self.tempdir)
+        else:
+            # Folder should be deleted on all other platforms
+            mozfile.rmtree(self.tempdir)
+            self.assertFalse(os.path.exists(self.tempdir))
+
+    def test_remove_directory_after_closing_file(self):
+        """ Test that the call to mozfile.rmtree succeeds on
+            all platforms after file is closed """
+
+        filepath = os.path.join(self.tempdir, *stubs.files[1])
+        with open(filepath, 'w') as f:
+            f.write('foo-bar')
+        # Delete directory tree
+        mozfile.rmtree(self.tempdir)
+        # Check deletion is successful
+        self.assertFalse(os.path.exists(self.tempdir))
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozfile/tests/test_tempdir.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+tests for mozfile.TemporaryDirectory
+"""
+
+from mozfile import TemporaryDirectory
+import os
+import unittest
+
+
+class TestTemporaryDirectory(unittest.TestCase):
+
+    def test_removed(self):
+        """ensure that a TemporaryDirectory gets removed"""
+        path = None
+        with TemporaryDirectory() as tmp:
+            path = tmp
+            self.assertTrue(os.path.isdir(tmp))
+            tmpfile = os.path.join(tmp, "a_temp_file")
+            open(tmpfile, "w").write("data")
+            self.assertTrue(os.path.isfile(tmpfile))
+        self.assertFalse(os.path.isdir(path))
+        self.assertFalse(os.path.exists(path))
+
+    def test_exception(self):
+        """ensure that TemporaryDirectory handles exceptions"""
+        path = None
+        with self.assertRaises(Exception):
+            with TemporaryDirectory() as tmp:
+                path = tmp
+                self.assertTrue(os.path.isdir(tmp))
+                raise Exception("oops")
+        self.assertFalse(os.path.isdir(path))
+        self.assertFalse(os.path.exists(path))
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/mozfile/tests/test_tempfile.py
+++ b/testing/mozbase/mozfile/tests/test_tempfile.py
@@ -7,17 +7,36 @@
 """
 tests for mozfile.NamedTemporaryFile
 """
 
 import mozfile
 import os
 import unittest
 
+
 class TestNamedTemporaryFile(unittest.TestCase):
+    """test our fix for NamedTemporaryFile"""
+
+    def test_named_temporary_file(self):
+        """ Ensure the fix for re-opening a NamedTemporaryFile works
+
+            Refer to https://bugzilla.mozilla.org/show_bug.cgi?id=818777
+            and https://bugzilla.mozilla.org/show_bug.cgi?id=821362
+        """
+
+        test_string = "A simple test"
+        with mozfile.NamedTemporaryFile() as temp:
+            # Test we can write to file
+            temp.write(test_string)
+            # Forced flush, so that we can read later
+            temp.flush()
+
+            # Test we can open the file again on all platforms
+            self.assertEqual(open(temp.name).read(), test_string)
 
     def test_iteration(self):
         """ensure the line iterator works"""
 
         # make a file and write to it
         tf = mozfile.NamedTemporaryFile()
         notes = ['doe', 'rae', 'mi']
         for note in notes:
@@ -28,17 +47,17 @@ class TestNamedTemporaryFile(unittest.Te
         tf.seek(0)
         lines = [line.rstrip('\n') for line in tf.readlines()]
         self.assertEqual(lines, notes)
 
         # now read from it iteratively
         lines = []
         for line in tf:
             lines.append(line.strip())
-        self.assertEqual(lines, []) # because we did not seek(0)
+        self.assertEqual(lines, [])  # because we did not seek(0)
         tf.seek(0)
         lines = []
         for line in tf:
             lines.append(line.strip())
         self.assertEqual(lines, notes)
 
     def test_delete(self):
         """ensure ``delete=True/False`` works as expected"""
--- a/testing/mozbase/mozfile/tests/test_url.py
+++ b/testing/mozbase/mozfile/tests/test_url.py
@@ -2,16 +2,17 @@
 
 """
 tests for is_url
 """
 
 import unittest
 from mozfile import is_url
 
+
 class TestIsUrl(unittest.TestCase):
     """test the is_url function"""
 
     def test_is_url(self):
         self.assertTrue(is_url('http://mozilla.org'))
         self.assertFalse(is_url('/usr/bin/mozilla.org'))
         self.assertTrue(is_url('file:///usr/bin/mozilla.org'))
         self.assertFalse(is_url('c:\foo\bar'))
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/README.md
+++ /dev/null
@@ -1,1 +0,0 @@
-basic python webserver, tested with talos
--- a/testing/mozbase/mozhttpd/mozhttpd/__init__.py
+++ b/testing/mozbase/mozhttpd/mozhttpd/__init__.py
@@ -1,7 +1,46 @@
 # 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/.
 
+"""
+Mozhttpd is a simple http webserver written in python, designed expressly
+for use in automated testing scenarios. It is designed to both serve static
+content and provide simple web services.
+
+The server is based on python standard library modules such as
+SimpleHttpServer, urlparse, etc. The ThreadingMixIn is used to
+serve each request on a discrete thread.
+
+Some existing uses of mozhttpd include Peptest_, Eideticker_, and Talos_.
+
+.. _Peptest: https://github.com/mozilla/peptest/
+
+.. _Eideticker: https://github.com/mozilla/eideticker/
+
+.. _Talos: http://hg.mozilla.org/build/
+
+The following simple example creates a basic HTTP server which serves
+content from the current directory, defines a single API endpoint
+`/api/resource/<resourceid>` and then serves requests indefinitely:
+
+::
+
+  import mozhttpd
+
+  @mozhttpd.handlers.json_response
+  def resource_get(request, objid):
+      return (200, { 'id': objid,
+                     'query': request.query })
+
+
+  httpd = mozhttpd.MozHttpd(port=8080, docroot='.',
+                            urlhandlers = [ { 'method': 'GET',
+                                              'path': '/api/resources/([^/]+)/?',
+                                              'function': resource_get } ])
+  print "Serving '%s' at %s:%s" % (httpd.docroot, httpd.host, httpd.port)
+  httpd.start(block=True)
+
+"""
+
 from mozhttpd import MozHttpd, Request, RequestHandler, main
 from handlers import json_response
-import iface
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/mozhttpd/iface.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
-# You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import os
-import socket
-if os.name != 'nt':
-    import fcntl
-    import struct
-
-def _get_interface_ip(ifname):
-    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-    return socket.inet_ntoa(fcntl.ioctl(
-            s.fileno(),
-            0x8915,  # SIOCGIFADDR
-            struct.pack('256s', ifname[:15])
-            )[20:24])
-
-def get_lan_ip():
-    try:
-        ip = socket.gethostbyname(socket.gethostname())
-    except socket.gaierror:  # for Mac OS X
-        ip = socket.gethostbyname(socket.gethostname() + ".local")
-
-    if ip.startswith("127.") and os.name != "nt":
-        interfaces = ["eth0", "eth1", "eth2", "wlan0", "wlan1", "wifi0", "ath0", "ath1", "ppp0"]
-        for ifname in interfaces:
-            try:
-                ip = _get_interface_ip(ifname)
-                break;
-            except IOError:
-                pass
-    return ip
--- a/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
+++ b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
@@ -11,17 +11,17 @@ import logging
 import threading
 import posixpath
 import socket
 import sys
 import os
 import urllib
 import urlparse
 import re
-import iface
+import moznetwork
 import time
 from SocketServer import ThreadingMixIn
 
 class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
     allow_reuse_address = True
     acceptable_errors = (errno.EPIPE, errno.ECONNABORTED)
 
     def handle_error(self, request, client_address):
@@ -158,24 +158,33 @@ class RequestHandler(SimpleHTTPServer.Si
 
     # This produces a LOT of noise
     def log_message(self, format, *args):
         pass
 
 
 class MozHttpd(object):
     """
+    :param host: Host from which to serve (default 127.0.0.1)
+    :param port: Port from which to serve (default 8888)
+    :param docroot: Server root (default os.getcwd())
+    :param urlhandlers: Handlers to specify behavior against method and path match (default None)
+    :param proxy_host_dirs: Toggle proxy behavior (default False)
+    :param log_requests: Toggle logging behavior (default False)
+
     Very basic HTTP server class. Takes a docroot (path on the filesystem)
     and a set of urlhandler dictionaries of the form:
 
-    {
-      'method': HTTP method (string): GET, POST, or DEL,
-      'path': PATH_INFO (regular expression string),
-      'function': function of form fn(arg1, arg2, arg3, ..., request)
-    }
+    ::
+
+      {
+        'method': HTTP method (string): GET, POST, or DEL,
+        'path': PATH_INFO (regular expression string),
+        'function': function of form fn(arg1, arg2, arg3, ..., request)
+      }
 
     and serves HTTP. For each request, MozHttpd will either return a file
     off the docroot, or dispatch to a handler function (if both path and
     method match).
 
     Note that one of docroot or urlhandlers may be None (in which case no
     local files or handlers, respectively, will be used). If both docroot or
     urlhandlers are None then MozHttpd will default to serving just the local
@@ -187,17 +196,17 @@ class MozHttpd(object):
     from <self.docroot>/<host>/.
 
     For example, the request "GET http://foo.bar/dir/file.html" would
     (assuming no handlers match) serve <docroot>/dir/file.html if
     proxy_host_dirs is False, or <docroot>/foo.bar/dir/file.html if it is
     True.
     """
 
-    def __init__(self, host="127.0.0.1", port=8888, docroot=None,
+    def __init__(self, host="127.0.0.1", port=0, docroot=None,
                  urlhandlers=None, proxy_host_dirs=False, log_requests=False):
         self.host = host
         self.port = int(port)
         self.docroot = docroot
         if not urlhandlers and not docroot:
             self.docroot = os.getcwd()
         self.proxy_host_dirs = proxy_host_dirs
         self.httpd = None
@@ -211,37 +220,56 @@ class MozHttpd(object):
             proxy_host_dirs = self.proxy_host_dirs
             request_log = self.request_log
             log_requests = self.log_requests
 
         self.handler_class = RequestHandlerInstance
 
     def start(self, block=False):
         """
-        Start the server.  If block is True, the call will not return.
-        If block is False, the server will be started on a separate thread that
-        can be terminated by a call to .stop()
+        Starts the server.
+
+        If `block` is True, the call will not return. If `block` is False, the
+        server will be started on a separate thread that can be terminated by
+        a call to stop().
         """
         self.httpd = EasyServer((self.host, self.port), self.handler_class)
         if block:
             self.httpd.serve_forever()
         else:
             self.server = threading.Thread(target=self.httpd.serve_forever)
             self.server.setDaemon(True) # don't hang on exit
             self.server.start()
 
     def stop(self):
+        """
+        Stops the server.
+
+        If the server is not running, this method has no effect.
+        """
         if self.httpd:
             ### FIXME: There is no shutdown() method in Python 2.4...
             try:
                 self.httpd.shutdown()
             except AttributeError:
                 pass
         self.httpd = None
 
+    def get_url(self, path="/"):
+        """
+        Returns a URL that can be used for accessing the server (e.g. http://192.168.1.3:4321/)
+
+        :param path: Path to append to URL (e.g. if path were /foobar.html you would get a URL like
+                     http://192.168.1.3:4321/foobar.html). Default is `/`.
+        """
+        if not self.httpd:
+            return None
+
+        return "http://%s:%s%s" % (self.host, self.httpd.server_port, path)
+
     __del__ = stop
 
 
 def main(args=sys.argv[1:]):
 
     # parse command line options
     from optparse import OptionParser
     parser = OptionParser()
@@ -257,17 +285,17 @@ def main(args=sys.argv[1:]):
     parser.add_option('-d', '--docroot', dest='docroot',
                       default=os.getcwd(),
                       help="directory to serve files from [DEFAULT: %default]")
     options, args = parser.parse_args(args)
     if args:
         parser.error("mozhttpd does not take any arguments")
 
     if options.external_ip:
-        host = iface.get_lan_ip()
+        host = moznetwork.get_lan_ip()
     else:
         host = options.host
 
     # create the server
     server = MozHttpd(host=host, port=options.port, docroot=options.docroot)
 
     print "Serving '%s' at %s:%s" % (server.docroot, server.host, server.port)
     server.start(block=True)
--- a/testing/mozbase/mozhttpd/setup.py
+++ b/testing/mozbase/mozhttpd/setup.py
@@ -1,34 +1,26 @@
 # 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
 
-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.5'
-
-deps = []
+PACKAGE_VERSION = '0.6'
+deps = ['moznetwork >= 0.1']
 
 setup(name='mozhttpd',
       version=PACKAGE_VERSION,
-      description="basic python webserver, tested with talos",
-      long_description=description,
+      description="Python webserver intended for use with Mozilla testing",
+      long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Mozilla Automation and Testing Team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['mozhttpd'],
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/baseurl.py
@@ -0,0 +1,18 @@
+import mozhttpd
+import unittest
+
+class BaseUrlTest(unittest.TestCase):
+
+    def test_base_url(self):
+        httpd = mozhttpd.MozHttpd(port=0)
+        self.assertEqual(httpd.get_url(), None)
+        httpd.start(block=False)
+        self.assertEqual("http://127.0.0.1:%s/" % httpd.httpd.server_port,
+                         httpd.get_url())
+        self.assertEqual("http://127.0.0.1:%s/cheezburgers.html" % \
+                             httpd.httpd.server_port,
+                         httpd.get_url(path="/cheezburgers.html"))
+        httpd.stop()
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozhttpd/tests/basic.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+import mozhttpd
+import mozfile
+import os
+import tempfile
+import unittest
+
+
+class TestBasic(unittest.TestCase):
+    """ Test basic Mozhttpd capabilites """
+
+    def test_basic(self):
+        """ Test mozhttpd can serve files """
+
+        tempdir = tempfile.mkdtemp()
+
+        # sizes is a dict of the form: name -> [size, binary_string, filepath]
+        sizes = {'small': [128], 'large': [16384]}
+
+        for k in sizes.keys():
+            # Generate random binary string
+            sizes[k].append(os.urandom(sizes[k][0]))
+
+            # Add path of file with binary string to list
+            fpath = os.path.join(tempdir, k)
+            sizes[k].append(fpath)
+
+            # Write binary string to file
+            with open(fpath, 'wb') as f:
+                f.write(sizes[k][1])
+
+        server = mozhttpd.MozHttpd(docroot=tempdir)
+        server.start()
+        server_url = server.get_url()
+
+        # Retrieve file and check contents matchup
+        for k in sizes.keys():
+            retrieved_content = mozfile.load(server_url + k).read()
+            self.assertEqual(retrieved_content, sizes[k][1])
+
+        # Cleanup tempdir and related files
+        mozfile.rmtree(tempdir)
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/mozhttpd/tests/manifest.ini
+++ b/testing/mozbase/mozhttpd/tests/manifest.ini
@@ -1,3 +1,5 @@
+[api.py]
+[baseurl.py]
+[basic.py]
 [filelisting.py]
-[api.py]
 [requestlog.py]
--- a/testing/mozbase/mozinfo/mozinfo/__init__.py
+++ b/testing/mozbase/mozinfo/mozinfo/__init__.py
@@ -46,9 +46,11 @@ Module variables:
 
    * :attr:`bits`
    * :attr:`os`
    * :attr:`processor`
    * :attr:`version`
 
 """
 
+import mozinfo
 from mozinfo import *
+__all__ = mozinfo.__all__
--- a/testing/mozbase/mozinfo/mozinfo/mozinfo.py
+++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py
@@ -103,19 +103,22 @@ def sanitize(info):
             info["processor"] = "x86_64"
             info["bits"] = 64
         else:
             info["processor"] = "x86"
             info["bits"] = 32
 
 # method for updating information
 def update(new_info):
-    """Update the info.
-    new_info can either be a dict or a path/url
-    to a json file containing a dict."""
+    """
+    Update the info.
+
+    :param new_info: Either a dict containing the new info or a path/url
+                     to a json file containing the new info.
+    """
 
     if isinstance(new_info, basestring):
         f = mozfile.load(new_info)
         new_info = json.loads(f.read())
         f.close()
 
     info.update(new_info)
     sanitize(info)
@@ -123,23 +126,60 @@ def update(new_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 or isBsd:
         globals()['isUnix'] = True
 
+def find_and_update_from_json(*dirs):
+    """
+    Find a mozinfo.json file, load it, and update the info with the
+    contents.
+
+    :param dirs: Directories in which to look for the file. They will be
+                 searched after first looking in the root of the objdir
+                 if the current script is being run from a Mozilla objdir.
+
+    Returns the full path to mozinfo.json if it was found, or None otherwise.
+    """
+    # First, see if we're in an objdir
+    try:
+        from mozbuild.base import MozbuildObject
+        build = MozbuildObject.from_environment()
+        json_path = _os.path.join(build.topobjdir, "mozinfo.json")
+        if _os.path.isfile(json_path):
+            update(json_path)
+            return json_path
+    except ImportError:
+        pass
+
+    for d in dirs:
+        d = _os.path.abspath(d)
+        json_path = _os.path.join(d, "mozinfo.json")
+        if _os.path.isfile(json_path):
+            update(json_path)
+            return json_path
+
+    return None
+
 update({})
 
 # exports
 __all__ = info.keys()
 __all__ += ['is' + os_name.title() for os_name in choices['os']]
-__all__ += ['info', 'unknown', 'main', 'choices', 'update']
-
+__all__ += [
+    'info',
+    'unknown',
+    'main',
+    'choices',
+    'update',
+    'find_and_update_from_json',
+    ]
 
 def main(args=None):
 
     # parse the command line
     from optparse import OptionParser
     parser = OptionParser(description=__doc__)
     for key in choices:
         parser.add_option('--%s' % key, dest=key,
--- a/testing/mozbase/mozinfo/setup.py
+++ b/testing/mozbase/mozinfo/setup.py
@@ -1,32 +1,32 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
-PACKAGE_VERSION = '0.5'
+PACKAGE_VERSION = '0.6'
 
 # dependencies
 deps = ['mozfile >= 0.6']
 try:
     import json
 except ImportError:
     deps = ['simplejson']
 
 setup(name='mozinfo',
       version=PACKAGE_VERSION,
       description="Library to get system information for use in Mozilla testing",
       long_description="see http://mozbase.readthedocs.org",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Mozilla Automation and Testing Team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['mozinfo'],
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinfo/tests/manifest.ini
@@ -0,0 +1,1 @@
+[test.py]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinfo/tests/test.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import mock
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+import mozinfo
+
+class TestMozinfo(unittest.TestCase):
+    def setUp(self):
+        reload(mozinfo)
+        self.tempdir = os.path.abspath(tempfile.mkdtemp())
+
+        # When running from an objdir mozinfo will use a build generated json file
+        # instead of the ones created for testing. Prevent that from happening.
+        # See bug 896038 for details.
+        sys.modules['mozbuild'] = None
+
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+        del sys.modules['mozbuild']
+
+    def test_basic(self):
+        """Test that mozinfo has a few attributes."""
+        self.assertNotEqual(mozinfo.os, None)
+        # should have isFoo == True where os == "foo"
+        self.assertTrue(getattr(mozinfo, "is" + mozinfo.os[0].upper() + mozinfo.os[1:]))
+
+    def test_update(self):
+        """Test that mozinfo.update works."""
+        mozinfo.update({"foo": 123})
+        self.assertEqual(mozinfo.info["foo"], 123)
+
+    def test_update_file(self):
+        """Test that mozinfo.update can load a JSON file."""
+        j = os.path.join(self.tempdir, "mozinfo.json")
+        with open(j, "w") as f:
+            f.write(json.dumps({"foo": "xyz"}))
+        mozinfo.update(j)
+        self.assertEqual(mozinfo.info["foo"], "xyz")
+
+    def test_update_file_invalid_json(self):
+        """Test that mozinfo.update handles invalid JSON correctly"""
+        j = os.path.join(self.tempdir,'test.json')
+        with open(j, 'w') as f:
+            f.write('invalid{"json":')
+        self.assertRaises(ValueError,mozinfo.update,[j])
+
+    def test_find_and_update_file(self):
+        """Test that mozinfo.find_and_update_from_json can
+        find mozinfo.json in a directory passed to it."""
+        j = os.path.join(self.tempdir, "mozinfo.json")
+        with open(j, "w") as f:
+            f.write(json.dumps({"foo": "abcdefg"}))
+        self.assertEqual(mozinfo.find_and_update_from_json(self.tempdir), j)
+        self.assertEqual(mozinfo.info["foo"], "abcdefg")
+
+    def test_find_and_update_file_invalid_json(self):
+        """Test that mozinfo.find_and_update_from_json can
+        handle invalid JSON"""
+        j = os.path.join(self.tempdir, "mozinfo.json")
+        with open(j, 'w') as f:
+            f.write('invalid{"json":')
+        self.assertRaises(ValueError, mozinfo.find_and_update_from_json, self.tempdir)
+
+
+    def test_find_and_update_file_mozbuild(self):
+        """Test that mozinfo.find_and_update_from_json can
+        find mozinfo.json using the mozbuild module."""
+        j = os.path.join(self.tempdir, "mozinfo.json")
+        with open(j, "w") as f:
+            f.write(json.dumps({"foo": "123456"}))
+        m = mock.MagicMock()
+        # Mock the value of MozbuildObject.from_environment().topobjdir.
+        m.MozbuildObject.from_environment.return_value.topobjdir = self.tempdir
+        with mock.patch.dict(sys.modules, {"mozbuild": m, "mozbuild.base": m}):
+            self.assertEqual(mozinfo.find_and_update_from_json(), j)
+        self.assertEqual(mozinfo.info["foo"], "123456")
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/mozinstall/mozinstall/mozinstall.py
+++ b/testing/mozbase/mozinstall/mozinstall/mozinstall.py
@@ -56,17 +56,18 @@ def get_binary(path, app_name):
     app_name -- application binary without file extension to look for
 
     """
     binary = None
 
     # On OS X we can get the real binary from the app bundle
     if mozinfo.isMac:
         plist = '%s/Contents/Info.plist' % path
-        assert os.path.isfile(plist), '"%s" has not been found.' % plist
+        if not os.path.isfile(plist):
+            raise InvalidBinary('%s/Contents/Info.plist not found' % path)
 
         binary = os.path.join(path, 'Contents/MacOS/',
                               readPlist(plist)['CFBundleExecutable'])
 
     else:
         app_name = app_name.lower()
 
         if mozinfo.isWin:
@@ -200,17 +201,17 @@ def uninstall(install_folder):
 
             finally:
                 # trbk won't get GC'ed due to circular reference
                 # 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)
+    mozfile.rmtree(install_folder)
 
 
 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
--- a/testing/mozbase/mozinstall/setup.py
+++ b/testing/mozbase/mozinstall/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 = '1.6'
+PACKAGE_VERSION = '1.7'
 
 deps = ['mozinfo >= 0.4',
         'mozfile'
        ]
 
 setup(name='mozInstall',
       version=PACKAGE_VERSION,
       description="package for installing and uninstalling Mozilla applications",
@@ -28,17 +28,17 @@ setup(name='mozInstall',
                    'Natural Language :: English',
                    'Operating System :: OS Independent',
                    'Programming Language :: Python',
                    'Topic :: Software Development :: Libraries :: Python Modules',
                   ],
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL 2.0',
       packages=['mozinstall'],
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       # we have to generate two more executables for those systems that cannot run as Administrator
       # and the filename containing "install" triggers the UAC
       entry_points="""
new file mode 100644
index 0000000000000000000000000000000000000000..f7f36f6318ace2e4adcbff5d1e2c1129987530f3
GIT binary patch
literal 13441
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
new file mode 100644
index 0000000000000000000000000000000000000000..c1b0564fde74db63a4cd0c50f4825e058b59e64c
GIT binary patch
literal 55015
zc%1CLdt6l2-ao$Q0**L1qe4+$8WYR1%tA{-ZBT}^#70MEtk5DE93}*Uy}gtuG*ArN
z=~&0gPM(v7o@3=nD>W*!K@HGM@seR`l3DF(s6*vJRC0aapS1_H^PHaV^L&4o*Y}TS
zUVhkXt-UUv^;w_Wx@|KaST5)ULC^yj6NFkp{aJ<o{jV9&`<nNA3!8g>I=EId<<r44
zJb5LSqT<52#o6;LbFvEx3PsB!ZcDLLV96`6q}}JR%qz@skBE(pNsQESYi-4y($Vkp
zx?t;(EqM!gIGpz*kH4FjgXiG$U*s*t^X-$Zc_N<E^X7Oc|L@^uw+X@&O_Wgg_0{)w
zrL_nqO)pKfAUuG`VGZx}CnFIy1BxQOrm$9}M|6ZP{Aba1NAS9gXQaH{&+Z5Rtt}CR
z%@k<T3X}dT#Qd-Rfafyp@4p@)x)+Lg9-J9Li2ADkZ61ptJUpT}CtJ)GgyntII)%P~
z*}u(a#lI11HeumL&}(_5uA={tFF2wgl9BRQ|2=|bl&`p?cn-pxCZLTl0Z;Kiw)^`%
z{GZ?do4-uC)z_NkD>a(L;rN>_+v22H-=qYy99uPhg5aN|w=>DiBqNgyEPhh4RR~Q0
zCpet)=;e4Hu&s63`Ku{-<#V4A{5E62c3MrdBTX)C^SRpud0<)*%9f!?)7JFqrmaq=
zX=A1$?;oC_m^RkwcK9>QFP&vc2dmrgej1WYTc>84HfAWgI(ZM?pu`L+QQe65cGE_i
zS+@hpbzd1wTZ6qC1GY1Ee?|vTL^D2Ho8E7Wv#W?0S^Nwj!lXEM$Y)dZ$)$>E*#gFx
zZ^=w1y4n0uz#gS4gD4|r%B4oH#?@duP14g~Qw(^ZE6Bj+&vc=#B3_p~dL{xcc0}#4
z$AWxzFY}b$J^`umGkJ?fyyxwLPAW;Vkp9r@q~L?QcbElvN}_(vdXP~*;K)0e0MT-~
z<QZB+`wp#mU*IuK5=$Aq0k2CY$oukk)B8Z0#vmKo>$IZo?(m@YI=$Fa-hW@^ZqX7p
zx7V4aUh>hTFMMBXl8*Wl(xa$^T0`-khh{&xLr=p!ZJL~E!1(=~*G}){frLQPZrv{~
ze?})rD5o3NYsFs48AdTWar*>+x*;W=BatTW$P02L7AF`26Ew1k@QCqhrYq|}O^WKJ
zVeNHRQO9hZ?R6S43ePYi9Vt;%gWOcSEhz{@cJD9<zS2%X{KZ$=A&AkwsRnEKJ9(<1
z?5m}|-5UQ?L-=SnZJbH%zH^q6QOUd`^KS~+4K7)`K3eLV#O^-yUFGAQqTZ+pYZEW}
zwdv(MF6;Brhi0o?_O%+<5V!i=o#5Q46&O*MJdX1CZ5^_$Bk5Rjf_QJr$r779nws?0
z8{{k6cSK9WWSgNf2n5Hj6vX~=MrTsc>N}-5)FMyq9MUM)A?47ilpRv9iRJaY;giMn
zDJR8P^xJ3a(4&u*;#Xu_C-1Xfkkb<NmBG?J*7pSK1fi}aW_aT?WgjdR-Z^D4(Gd2^
z^>XYcpzvsLfUeFrMk8j~m1V$p94Ne0%(N@D2vi0oy)`}DW0Jg2ZW~_bTbyVV#9K5`
z`QSaTz(&u&(=$>6-;Smv7C-Vat6=wNfguwU6Igs2xUjOl^tq(qYzzfexitJw!cLAQ
zI-w4+JYUH}IxzFtU$Ig-gK|$YM->z;Z(R#5u!d9wSV=Otyglv@ls5Zqdy|g+vQD-g
zmor=BHu-$rw=ul0{#l)M-^J*D(wz%r9QVggdKro#quEz?(RVJ@^t3h(h$RO8Jj^OM
z?8@mef)FZ>MN-rhV4pl|uc=}Vq7D|{fqsNs2-}rS7a5y`+`I4$?d>N}$=9jm778oB
zqDo(fW~mM%W@qtLq<mh+8z${#@tssn0I|9jZSt(s(zSAxa2L9*@mE>#RDvih15bT`
zl0$QlQ|52{JYQ%!CD={}MpaTYDJb_&`NXs$I3z=n(<+bqdxyqSOQW8rx6aUBm{vK~
zj;_d~9|qx5J`sCNpT>?5m11*{jKUQRne0dmwIkqdQyw6aNG(9Uf~zJ|<q5n9y=LZb
z#}a}aF6Gs`@N7%@MV#Qxaw@}l%;Bnzt{PNj>&4>Bcu}c8i(ib;xOc%vQZFc{g@~;;
zOC+Y%wq8}y>38mwwkOYO5eIp{RboL<mq%C$I&7OLkS-I6c(Vzwe}+c(BI<7IM?<hO
z9T<&<m*j0yUPp@eq}-;gp+G;mLs^M{%gN%4I1*wH@7JMUVgwB5X;Z~16I7it6Xnsf
zio+(#sjRZ1<mu497;51GB5_;5X0QVnQj9D<U#(~eWHP0G{?IWA;$Wu|hdNSrioJco
zn@GVbJ%Mt#XM?R1>?u@TL#<WTz{la0xX>97b>>;5c$xA~^eK>GnC9ef9d>Y7PK4Tp
z$PqFDG4DT=eKa;sW%FIcm!OXKR^<a4(>9Etuce36FLn5B2Ia?bKy$nOWsar6sVsqO
z6TXsD(y2T)fhab69Q`eyK)tU^6NHcKHLzBt2HoOYoyue=f~4K(j5&~@_=0iMF!&Dx
z9k~%b_62_h+Y4nw>drRz4_eV)@+^ZC?Npk^qf`)eXiU$8)jh~$bt*^46Z<pBqcoo~
zIy@wWF6H}FG|j`x$yDO7HZI-D8AM!adP6EP_wl+8on3vm;7-(~=uH(4=z~dOf2a3+
z$kmCk)Zsm)+&)^M9%w@k{EDO~1Ib5<nX2yaey=2qCNUm`M2CY!Pdcr96OW`=O7fK|
zOzx1PeWk5T>>IEtFxZO2&P8-Rx3UPdB9L;y6!;Qj;bRx2e%_E%v2olxoXYJ8gbm&w
zoyu(pdCc%_ouONMVlcy_FidfMDBp3r@=h|biK!wFL`<=P1MksLpw(3Ql!?0A7V?|8
zWSugra;JQQ5}`aw)F_{tY1npwKSLGZf3H^g3(295Mtuc^G~oC3X3pGx+u^D~Vowm=
zRB@w;$}K~=lnWT-4pUVEQc^mWXl0n4!@34=)l9sVOAY8^#f9F2P-=224<bp|u_($v
z%OGDUXT+Q6KVDW1xFd7~Pp?I}lbXF1@^&k4w$<*G`m*>oRp5)DoI<s|$!|G>lyEC7
zrAz5yj);NN4#c5MRe9<Mq{V(t?wy=b%SZW3JE21U!Z7CvxqFj5{+&|-?X5LYdu@^-
zrERfc$qV7$DQzVN*`|ahA>aI`WLs-!d~Z&*$EgT<sys#{1`{j34&#D(EU|Lk4OT&S
zF8s*wbK%*hjr-+uVHcF*1i9g*v*+ZF@IAfGh0}+e3r|Qo7apG!1Zh?T!{4P`ka~v=
zNh~}(<$~CO{AbXs@E`<wss+!7`y{c5<cFgVeHYdqVy@8Q^AK2#64QmuX&`4Oy(Eys
zy2}`<jOEH)9%tWZB~_9(iN@k2)S7%(%1-GgiXA}A*P!RZ4NbKc(zS-N`+|DAOL@L4
z>ZEw+0VBrvkT{eIo`W(A+^xqr7a^0}!&O81V1Y;r^+L-YJqE;U1v-xr!ykvvOV`_#
zoUSVU(f-l&41H-Nj#GvbZQ<PRRr({;ZfnzEEa8MDDZ>y)I+DmKOH&ef+~6y19_-af
zX-YUYLV;7%;f%Jrb6RmwAhTooGzX2nwAT*ps0UFZ3Z^rB*QIoF7{WIxyQq%7sN(|!
zstxT&`|lpoSly!?C8iB&u&eo5yaBo4StZXpZd!Jok&9~ROb25QC6{bBiyv0ZwQR)<
z=nUD`5;7XOR869{7%Z!r>n56Q^n+i$rV)OIZin3)b|{JJyOH#64}@_#Z;`T+xd_^w
zk!go*uh)!O!fCr~YvfOdLy4lYQ1YXY!S3)BYq(vZzL9QN+>6)D=P5-b=99b+@XE(f
zb{22N3p=oU)u>|d7?H}WtC@k}Q36P;$w8>v)(ZVm-B|XOratOXBxtH)J(P|VZKB>&
zgm6`C9{#@SrGk%-1ryBROhJ>7(2yP^ZsUy+aT{$UaZ@1Pu;=7Ggg%QQxgN{=&rRhm
z#)v8)QhTqJo0WkSh{rI+QJ_`1vz@W(9!yebptwQ8U{yy`%rT^1Rk&H`j;~{|DIlTJ
zP^y7y{;U^SIj1+Q*l!_|WgJCX6ij8Rpy>`Nj&ymn9woy)ye35f(|U7=366}@g0^Bf
zVCzt)JqF}~gR!_5Siz<2fJF%#lV^2^JtFeTGm)wfwfE$#@Bj^la{Ks5gJM6qnU%!3
zl&4XYl_Z4Lp-WIv*MwdKy}UmvH})b^-URJC&93}18M<;173`?GCenhbVpUJ#u862b
zx=}@GzzHI7SlK$6{1I_fz_tyRXs_L4UQRM;P+nAD^aoS1B3O3qouo1lHoBB{*a>*I
z$QBw3qnJE!3@lq$o6EKe+^Q^#i43QYdqAz!qgb$>5tzsw&}?QererMM9EppMqdtgT
z9L34M8%LI9R;PF+S<pw%z|tN8LbrBlyI~#ysCr0Ia3jvuJPqp+->$fD3@Y=URhHe%
zS;<uSM~t0IX^d07uSx@V@)F1=UWBl>J#;TB^?nyh({SYX(}<k(+nSV*`PiTv+bHlU
zh^@R$AsYy-tVhV{w{26t9YqyOJ-maJ6-Xl=&LfssK>&xRqJ$W|LY25>UkF4;Lm+eS
zbk7RBkqvg%hx%_?_O|-UX_pObnw`w0fio_TC(0SE+ZpAktLrcfsTX7A41;}|+=opc
zi&w~7cg`%E;Xq9vh3OqU#lCVTB@a0w8=x@2l+)4>#h_D@({AG7Z9F_6<-+1ywom$_
zHDW8h@N=3zin0o2@L=KxJjQKiZ&EKB6}bdLZZ8kQ4jR+|?0vt;;DCbf|BN}xKw{K0
z;h!DK5~6Y+kWr$*5Xfo1fz!^Q*ity{VjbRd%JJb4#9BRPHN0HaU|UV=YRXv5a$Hqe
zBQ?rx2vuc`(V*E}IRh%rFRk^LuJ_xv=K3=>C@&8u>aQX<@-bE4z63iKt{E5G_V-@m
zK<+Jh6^C;64wALyBL#IP7+MldPFoy#avlVe+LIXUVlRjCF}lDfb}(-}y?vA|6o;B`
zMeJJTZxn0xeyaSX2dP=Iu@+*gyM7@H++Z^dp1MR(%6mji$U+L7Ahs%?{2p>P0yN^F
zH~2A;SU)<&W(XC6-O+|L9BLuMpiX#96|W#Wtln)@7A++}cR1w^j1W0I*8rUaLMNlu
z_mviWAhjsJQch(8N104>APVHYUq^|jC{b#qfo$_?38iB+tLa1BOr{@m1jjTvb|m?%
zyS0vK4u`|;>e5{CmdKQ0EGqO})Nr3(>g^q(ybw)t7PuWAx$mMLNjfDLNzNL|G1GTZ
zCz(6~G_(uaM^QeFA;$!rM{FC{hkDf;(?!(FKD6e!O3(_*-9)-1&~Q{Vxt3LfOcl$u
zgl0#Bt%K+|4H!G8ImmJgs3d83D(V>pH(f7Ym7HM^qse2JFq48N>V$+;4Hy}IjNP6T
z1f8Amwveg}gzFy8sv6|a=nQA7?d~V4=+N#fQC;mOXt_drk6QJkX5%y`wQ8VIxC(UQ
zq~l4(2${YG)l}EgrH)oe2Ob}Tx-RI*$ZUuDu!nv;2d4Q!aYT_GJIy`vsN49Q##HgD
zhH})j5nG%V`@mr^A=|4oS;1^S91G6uA4TKZceGk@i6%0J>Ws#zJTOXiLx??BqaF<U
zti#G~Jzq0`6J=Uf2wZrlQtFoI{26<xnrAQo&Z=3xl>Vcr(|e)ymXylU;ef5l0fTYY
zt07YYXGB#JZeMAWMtTS;R~lSa+ACHZj=wSD&}!S+D*brOxiLo<ttryp#oNQlrmeQK
za3$9#fhjvdQ)C*0*S*Kaiu$oJQZ$ta_k#*0eS|hfk<rzm@u~siQ+^4Z_y=QzVy%|w
zr8poeL{0deg-N`tqf`TITJ{2RgznI)gz;;M;#1TtV~83H=*=}^LZC7WGhJYGi~4db
z?~`;_*UWlQC_~E)^;;yi0m@ACRV()*m}IWHrfN_mZ)k{?I9u6^@e5Q|QMJy{Xl`4;
zuDNglm3$p{H2l&wW&MbV_4q+?a(?*7E!0kAUaJ`ynb*R0{~1YZR?xcQXtL-q?J!8}
z2)Cb4IFwg=5-AS5l({@`8kYYEsSdx?B#uJd=^(A(f+&R(<lb_+-dAs!rfb0DD5Ft%
z0FCOT#xaA4_nK6H9nK6*P7k}da1!sh)E6|hHF%lQs|R`99o@bR931a2vrGGV|1q0H
zIzA0WA~bP^zB@<$uoI+F7e}c}zC9ogEi0T>*JgEmH8N%A;wxq2Q<BM7pWt*U*Mp3q
zCe$%4Ec$k{;f?-`)1r~4I9h7HC2VF=Yal(ru3UqhWm9`<Xm*p);x|4;*1zI;)!kB-
zhG3Mc1oHEmG(=U4$#>61k|{+TLV{(oD+|5{k`ARFS;G%Il()X;ETOv<G@C|Q`pWUN
zUP!esdy<w_!*{AHh-`B?Uu9KZro2Pt*oAQu1jmrZY2IGk+a#YG(*qb4zAO-L<t}@t
zhOZw))`<1O3&X6!@;Ixo3h*vq58w!(4KO#}Dm)K(2Qcait8fu%$p}9Hup#~e;5=aP
zm3R+mPOu8c0I^qDg=+z$09ydx0<OCn`WWyHAiBR*_<IEA4S?ScSO=&B^dE>e0d;`>
zgAfLM46t8g6)YC3P(0WwoB<5E7We`F2G9+$3L62}C&KRstO9HYL=UwJp8(DS;;yp_
z;{l5SrPrej`YlHw7qAF;l>*8ERe<LKF9H4xSOa(+uo+MXI0`rcP*-zlp_Pp)_X<ki
zk)#o`Z@IMCT-olD>r1s~o8@@E9^2^q%2d~(XQQg?T4JJTs%uw{BY$PP+)*0sQp{i{
zxuI)Ky}Z@P_kjYoPM3WRnMR`_=@@2Lk*y*#t=sIB^5oHE3qFcM=I~X?v&_=Hfyy&r
zGZLjv<sEf3yRVw&qT-$0wf&3&+&n<XIB(;Ne2ES=ni`5U(G)gzXM*16JYy7+2T~U4
z$1XhLR3rLj_@uh}F2$_TBLm@6ruKv!RNjGJ2zW7rQcjSLTEl5qq*rzUN4we~ni`*N
z8e%id>e^VamX{jMs7t!aD|pV6yQksJwsBcE_QNaWpocU?j!oEv0UOqqDx@?n*2;}`
zZ_c#H;yk9Vo`mImo*7w~pvkPdDDxQUo}k?mtyd*?*LAeyM;l1ar1oTtq?oYAQiMwD
z#U3cHAisY!CCuKL%%8Lxc`pQU_D&YRjK(Eu8EvHuae7jQ!yI}G+2}G7+W1x!^+wri
zsE|6{oV}BGbC|=Kbj;<S)j_@puVcHgAu`X1Qb<obYL@XTR?v{?NL`mXmFYK<x8-BZ
zUwXzrE)M;ZVruoz!p=0)+<UvQiq=LmZzetOA4XoFuxWyzq#?lp<+_~nPz@1GZZkde
z9V-1`7*JES{zV#%p~4biKwI)DZ6Ilf%b_gLsS}%PNvn;ctb+8ocR;|_?6524H;_Nw
z{8}5WUlx~_9{<Pozkey^m6s}$3ra-k;qubcC55@7<SH+1nJVSyOH+H=$gNk$&ex*t
z8D#;hz)?)8_s3G@ra#bJrQK#QNYVJKmr|LnGf>o-a&dlN`C_;?(zGxSf^r|Y>i(3`
z)SJ>+^7%TH&@J3zXvR_H!_QW>oBm({VoArK&Ml;i2T}|7i;iT>{jO!BDVwre%N=E}
z-mcI|0GhGFg@dOo-2~0wHNlnJIF)?C0e0m@?iI8;;XEk00$=zP`_zuXaD*LbX)8?d
z{o#MK+nP0gzN;%o*Q~@tjAl`*XgkOdNKd=prx-LNCrcghL4f-i)ur&KkM-5-y;i4Z
zH8Ltgh6$InH)os~v^^B(l1t5gsiTrfnDBSdK>*r{lTRWc>H|v~>3&0HaB*)rwwYFX
z8mB3z$z`*}Q78YVQOA>(Y};*DmYFJU(2(CvT=?E{S|hWaQDQ;Ka(5ifUu0Vx8PjLn
z$hDnL5+qA;rErzvKC*y(u%OOJh37zu8>Be@xb;-Y=nWeQj}EoUft&ERW;3nO9ONE}
z-_~kcww~skqv_S~UsMFz`Qo-l?hq#qx1AZ+hztwhUB?Mh5}#BWhNBZX_HHTP;ITaW
zTPfcFoujl31S6E5NeZU4i7Cl8vor{K+w4xRPPwl^)BR+rO}a@Vb%u@jbA!|&b1Z$O
ztr}D11av(tCEMamf9OF$ueUQi(0eg_RV4%aL@0pH-!E|gYLfTYnc>N@)bX#xN#^gx
zmVnLdh5zWf9Dz+Kl399#w;e})T>4R1=a)LWHaN{R<QQ&r%1C8NdyOWc?{<k}k|m=Q
zMNYalDL8x94ujz3n~jOau8hP~WT3VSYe_Y%8cwQVMIy&pJA0>7>5s6NmhEVB%jpqT
zgYB-qj&xgjYZ+OjOs=W25)&HI63q{T*h%g6X^C+b3;{}bG-bp)o5*cr@ryACUPBEn
z*(g7ushSGf&<vy{j`BQAlMIJv<QdY0Gl7+72I}J5`^ws3RL2IgYr^sg<l8<sjPFm<
zdQRCmO1rj(G^*Sn#Zcqd)@qQ3e#Y6A$|2mL9zbiPX^8{EbIQ(Hq-bl|7>jtmY|OPB
zSHyX-vN5+vNqFUudRI@<l<l@qUY&2BwtURgiPI-is{T@{zI@?D%Pnc+#00ga{%8p`
zN*Zgm;m%K_7sGx0GcWprT4>o7=*5@U3&Mv)=1g_DD1H*KSDw7cEwaT!^M_0pzm|wL
z1zpcncB6pu2!Agw;gnZ>TBp(sXEtRgx?`5y$pcAeh#3x-@~3Obtq=DsA8&c+u5n`2
z)ReO&zoeX{fgvMMoKrOj+QX>~8^U?+MyHCH?RvgRMLUppB8agx`(ke*!>^Q(kDHd5
zpe*Mqpp4u|yf#&QBSf};sO!qEYiQ2E;#U*$R*t;!deR1DZ!){~NyPUYnJOP;s+*i+
z22%S{uLvF=UdQ*#2dMjE8xiwfo!by4q)iY~g@?9}gd*q13fLwKSu>u)sgi_{67TYA
zP}2q?(<-&Wn;lMN3pyJqmdJ}Oq+-gZLEOjbWmo48)ai@7sr#fqIWm={C>4rkWD;m$
zMts7nQ|`Kf_5x%@xt#)0a+`890x4lAqe&^hh-sJyS;5N#?A~E4ekHA5v(JFUO-M_$
zdv1D`MBs>n*nJr7eTs=AF*0LldV*&ftu?XF9Yn@NzFdzvSx@EjlZ>sQvA`@$)YDL?
zCklwtYe<ACy?3M&zF02Q$}sYadU%BOAY%ai-074@kAh%@W9V1`bL!>W@2_l^exA);
zQ~nA%@pJMG+r_J0@<sV}w!omSQAXxm$gVu(8%kLr`fGS?z2r%TlqrdNF-9hIf3l*w
zq+s|Q_ja9XeL+cQ4d3K7c9}`OtUD#q=r1z(C&6K+iu{HedZ4Jz*U0FEgs(LYdPP0*
z<nl%<AA^~aj$yd^vL@5JaLu?H3S*4I_k6Ta4;|uicup6Sx`7CSW$=3BLP*avWEnG%
za!6M=Tpn!+vTUP#5VkTUX$aqirMhN8%_&nc25ZO?e@Oi3Yq4b3(2;_Mr~!A;u{n*P
z{CEvFWtHV*n5Irj`9yqNc1~GSi!vWsMgR|8lS|h)eHxperhEozTuq60?i6Q?y;d@P
zG#C-@wJzmnj7e33OF1=!P8f~pOlg>ZooAbBry^U5S455t@L^wXmM|Y^9z+v78Gr4W
z%Cjh%sXWBdfyLOGprQDmk?OHR<2vAHc;|MxnY8~><AEmiEwQtYEC`q?{M3V3dS5dN
z31CXI3v3wo(9}oJ;Y1_}W{5@XNQ7$A_^Fm{7{6RH<O9fAKwfaMcO<lMF>y9;3qJp_
z;c1B$hr^EGNCb!3iQkFY3Amr?uzSk*N}ieUveWP>|2WRooWZM|8TyD;U3cJ5@<8(a
zOqD+3=O{LsvZ`*fJbE*!noV@->j;U8XDtuPqvP=Wa3%h@G9CQY6)w**j^GiO|K>{_
zS@ST04qq_Ovu%u308?Rt6d_jvSt%0l2afZ-%<Y|w$@Ssk4n@9#E6#-Efpk*FR5_K=
zT9$Gf@`cDDK<4WZCYNm1gT7sHJHf@SkBGrOpu@f{W!hkJXm+6)NSaNZyUUYv<O8OS
z`_6u$iw%-Hwr6Utd_!)MG9;W^`GXh@(Q6G~B~R1(;|XVZTC_iY80Zq17E^gp>YH1B
z0j_nA4`}f$T$6Mx{E9z5i?DlF14G$h0qHKPW_0ObvV;*T(ky2UKPAQ&J-A$U$kF69
z*!gM(p{ky;x=;F!`m@v5YJQLAUJf}caX_Xs<*3V4kwO!$3kKRUueb$ad2wQ5(ht51
zcT4@e{W9@$bv0N;OH~)5qy#X=O_OLYF=VIx@-Z#H5Q}Q0tJ&z8e3WQjMR{u|8tO#%
zd>8sieW*&l9SOH4)Jspz_d&y^N?M;Xy(=c(1Sd+}pzKKbRUDX^>G?(Ai=VUzf=5MU
zasE>sUq6;~&hQ9Y95yE3ycROJ<Ss9RC#QWV<*YO+`DP<&|9F=&^$X0Z##uX{Di<n$
zAzF`wNf`45*KtD|qUa6Z8*1t1bwaDFGt_q13y0E58~yjuA{_chBuhD3lSN7@Lmo|-
zdo?cqI6Cabj(D}Mnj$?9G4B9Z%@}A(#ipa0Nus?!qeo>ljnpe|9A&JA7!#D?)yc8c
zp`;(=pX$Di){UkqL&~R%$NM@o(yf_nzF9fj%%v{TtI713^uNV6DVTgS=kgW7&{|q`
z9$~8Z6P_NTxncqaYp4xP6tz*;60K`nL!p$YwP*&mOQZA$K9M>q2a#%}H_1G|$rnp=
zr1^>R>^P553+6mEh~gH{RS-6plEdE)@bu>~Cyx#DT+L&XXdzS=8Dn*TU>9!^4Y}oC
z|9a8)iC+H1_mzR4<{>87Ih4l5&WX>rapj$uR@xO>CG~x+hvFK;eNK9nazfN)hP7Ue
z=ixT!Q?#-8F|L&t5-$!+<3p3_IU{gKWehtM;g5cqUadz%C!W|LwZ3pP8%@Ke>__7t
zkOAT{=unymQeP4r$~Oq;cG{KDKy~s>0*OH}D35$jg93i02`PyF{6#uCtB;Zfs;!wS
zX?Y|0W`Tzn(0M%VWt^riM@;HWr4f7PV^Z_!dM6uIxgK=Sm7{ZYQKUv(WJfPwhe-LD
z3!16oIqKc~o4nC><%GZ$z1i+LukIh*q;AcNv_wMbNGc-ci_gpiK`aEBU5Hev9WpA<
z9_iu(Da&i<dQF4li6UOwNn&%&<@mGFBu@UDNd(YDK6!XI(~_ptKlx_LCie5jxYRlt
zqUZ&QUYFW6V`k)AoTy(YMXN^|`IN`tF>ns&V#PTe?g~wX;QF`+fI}dQM!vQ}ykb=L
z#R<X=BjwPmL#eU|3VAhD9h|)1*0qKZgEX1C=2-UbSL&Hq+Fy<Olt!~7%JektZg|6V
z*gJkXUq66FC~Z%V)5_5<dE7E2i8?#`94Q*8Wc64zKNQVRhw}r{7+AbE_-!3xG_vA}
zsQ#g@>|<%ZoE~Qu2g~WmbVg1$(!s#UnF~6>tsdX@ryKE#PZ4QF56JQbn6V@ll3CX{
zjn`JoPly?noBPu~Uz}f~i5knjwpr>#6djDL%{iPdQ)Mg}gIRI%6C*2GX&*V3Oj1Dq
zAzfz}-Y6$?mx+~((sg*RHEr+Vx5dd5<1m5sxjR6LyU!k}`!UM*t;W})!8E<W)>$*&
zD)eeorhs~^WQ=zRt;^kYkxZ>uQ`rt36&0iBM+@M#uHti~iFoUw;pA!D9yB`?E3NpC
z=7NlI^gKxC5ZMtvKaj1S%3x{Fv&|sOGQPnfcqXdv;kIb>h}zVsZ|GQlfKM5GwZOBD
zR?|62r8v*zu52^GHlR=_TV0kUgCGy&sBjdd2V|SMPBD{HRXIXKS(BrvjH!}lIC3mW
zabQtgdk>4LqBm(VyQhsNUNP#*Bs*k>%VPqOd|zF(_iFT;lnmP5p@kM-sd1{Qau4P1
zBEliLG_HK9fwoPg;_{_N4K$#XC*L<fb|v^TI)?8lKPMP4-^(O|ljB-4C6IAu8v1uH
zr|l7W+)A35YnRfJ0Q+1XxPoiPL+~{zLF)AEde*Aan~t}5gh)nvSJMJDeJ`3Od6g#k
zmYQRwK71flTdA5nB=?tkQ<YwGR~oHh3<3$s_>fo=l~AWky)2=;49gH%6Y-5BB`K^S
zEi6T?A;|@P8m_b>5=m>U5niR$GU+-tf24OHu~aL3DLH*CEy&X()ZRrqC1|F(rrOvD
zG&Z}N>nE+DQF1u_widZ)tZW@q_iaphJ(Z52>+;(=KcLyY6zg;-rBsnJ>T1pqd?mYB
zE;TqJy46W1JL3CsFVpN)ZsQ?3cDkZ3omd#hRV3|4lC_X+aja+*d<A7Rp-r)o-Z+<X
z{RMtlRGERNw}(=7wYvOPpjB7PJ6KbM7fqG>xzA)Xr|g7ZmSL_7nRCGeI#Xq;mM&u$
z$tf5Y(oxp6d{UVdygyL74MY9Q8&DE_?&X<8y4qIjFAX->_G)cVC{mNs_exr7-4(Fa
z+8b<*sodY6p-upuWKgNI%Jx2yxw%|6fG-k?!^$u6H318)((Qx0?c(z)SN9r14;SX{
zSI~ip9JEafb_QjL2?nG`1YPuv9%3v4WL`{_l15$sB;A4$pktp-C9$u%?-|)3{KyJ+
zq2;(MXr}2<99O9)q0We-lY%J?;>~ooC17g>G{fp;o<Z6C%44D~cd*W-jO#;3G44${
zncJYt5~M>($57Re@uVW?U^6DJx-tti?r}S*2=(M`8yU}SYNzg$ZEf56xzmj{1#EUE
zvVbYm0R!$;TIAAZ*>>Etp&_PDiHW4b+1JUXMqbhzrF=+k!K@CkS4u|-^-IgAc%*of
z?>CFX?cNLg(1+3@MDn@3olfN-Ro|O0ywM7xa)h7H>`d8NlSmsoG2o!-`{Dc2!G)H}
zqBxPxN6^WIPH0`ba=e4KFgvB#<bgCGQk3!p#KX@cbQMPxnF}4=@<~EdYScI*DsTWP
zOp*?T?i?TjQO%90w<E}?m2Qqy6K{_k1_DOGeO+q{uv(;n-CafaI%h;Qt+W_!c6sB#
zRDxM}$b0<3l4x(tL*5e)A_XN%qP$-wF+`G*U{*{?L3zK`8+WMX{+G_4)3pU`E#8xM
zWfB|#+Anh0m2ni5>xV+V1bM$VCh1^6U#1HlQtm(dlf3`XkJFu=z0iQ$7xK;<or*Dz
zR7{7`(T|Q8ySkR46V$a@l4d2>C<u|gG?`!gkz@G{p<v}fF~QlMYS4&1XhB7%f2+KX
zrepU=-*?}$851F+3pxbqvZp7e5BX_CI6)lC;#Y%YrjHt7V@w|f!OfiQ{26Vk+eR_a
zu8&%#-PVySMfn<8co4?lZ`&KNHPUM%y@m|)E2~FjTkAu!=|T~2-%c0PNO8G5=OR|-
z2=}uh`|?NB>rN_?U?DE-{6bI?i|9W;o#AdXRbH#*C(yjwnik~YQ_~0irAi%qb+6Sw
z>x{a7<SnSlC*npN*R?dMjD46=JfBR#YvA3=cPVLx=<-H;2Ybz4bB%}lZ=-0<fC)^f
znZXCBmCmG)TQi#C4o^>he7%z$ak)GLy28E=b7`#0YxeZ-N<l%@g-SXGxs~-Rp&zdI
zU9dnhO3z@<eI?yraCs(b_(@!L#9{YL>k3cwT^PK0H!PQo9*M7v^v09SN&f^#iLZ?E
zUO}<Bh$XS4W6Au=l<ApWf_nYMZt!Qe1}2!x#=uLbd!K$=qpbB><fi-mvo;Mm&|urF
z6&PEuZLrnS<pqDnE`RB9e`Zh|6R>SUVuql)D?%Sq?#=2!P;^L+C7L$a6iV^y-&;db
zc|dJ=#CpBh$Klm+wo<b;hp$3qUA0_R9OdN9*1ID&!qnSdnXRT}_tMS3QiXPG+xW<k
zx0`su9*NO7`AW|SQi3;%TegS!O}-B0g)^Ag_&8H_Dt<)dS*JPEkW!cWChhi@no}b>
z-=7f&O_&i!PETz=(*8Y@@ure?FcI))oPjx5r02^Nm89_6qh11jT4_Gsian|1BEv<=
zleC+PJzqDC+9aPMrD3r{Zj9UooN<*wj-gwBR=RW-SN;>NVpZ&36y@8k2^-*og>}B4
zG<JWC^2Y|o_(EdR?z2r@NY*KaA$9%?Gko3T(lg>z*><LUp`ic{snnYlWw|_8>AMKZ
z-+1ZGFyBI`z`n9ID{>bwEy3V3Si<H@DOpGvax!AlrQ+Q3Qu8@j@JU>rxO+KHgKm$}
z(uEw)-Cdl>k9N@{*k$+FyAtJQ-vuTbz}CrQS4ujs<w1vX3Y~GdJeDpE+h8+uJ}Hg6
zWN~RjSTdq|K^y5qm#=o)8j>wY2+ccgqE+ksOL1CR_K%|s{&Ad##*yqtEV!wXaCNzq
zck5wdO3h*quR&>Oq;Q;g7aL6llxKh(`Ss%(XfBYnJLTk(-f||ee^S(>T2po|{@FFn
zGqtOG{#k^xg<nll9!9ksndHdT#MAu#BQ)K}HoiM(dS(^Pe`gx_fu1w6z6xlIaxcFv
z*#5Dm!M2UAs)~IA6U_+s<np+;Y>wt4aYvL_H#6K@u7ht5ZZV5GUr;9>U{bKIrI+uN
zq1O)iK;5}0UC?*R;`>U|Yey=4rD+~*mq^i(XqP9pTjPKscJkY!wlnJSCDXFGe7(`d
zg@}&xtl@@!p;?TBEAaL?#=7l&8;mb#O5K)5tDLV9Z=y>~aTfK894vWVi%#xvsoKwT
z@$zYtUA<GQ8uHtIy4;YXRxO|KNmu$x%@(QDL&B&0+(v^Dsua3;V>|J-G>F%^K{eyk
z_{j}r^f?+pzhr~}&x)h{%9P}Rq|n7A*LL!vxsT~|`FW!9l98HI+UUMOwDKN?5dL5V
znNVKdaq05(XjSL0M{Bbm>~;x8uAv)%4mfBjJIOIORcxYTZM3Pp-3~uq?tta;eW#In
zq7J7r#Yn#5o%~XOkxr5KCnDU4xXxx6N3*4p5^m_}3IGhK9zH%BJ?ZwY6;>o^r8I}b
zqleq?r^9WXsx54=HF5QOWO##rR<qwmhZ^m*bY*CG13#(*v%#dpo_^*C3#43tp0XLL
zRuYzxJH1YI?KX0^7Belr!P+y5_E+IPKl%?iNlYB>t2f@)p5CbuO?>C~c18G@drwAn
zdAE{DEn!TlXxhZD$0)a<pt{+&h7vDb>xDyw#)yJR$GXgSX{V`DCJ#HKiTN^)W8ed&
zEf6B|KK--y`ez+R2j6%@b@RvF#5Z4vlP6`#)`vl4e+CF^W}kb01k=^n`2@#+Xa_|n
zbw$}|dQ(G9(_z^~KZ5!7$|voN)zIx*a;g0T`Q=-Z2fC30%NoCzeCUiemE^-4$)Rz0
z`qNMVS)yt!omarD`t~j|W0)7iB{e!c8gsX~N9?*w8O)1&gUcMAM02;g<}<JU#LjR0
zz*u_}seYX}%AsPd+B7v)E5&*Z97jqENBalz9Gqp88Wi@lMr7}Ek06Yc<PJw<VeyDs
zf!<W{BUfj@pr`smMc2Z^p#$nt12>Yx=st*}yGB)-JsDH^5z6>RzC+134!hPSxH6Ci
zkO|T0rQ5skKjJl@YL}w>fh!W?5h+vM0}-$DE>su4`C0kOR+@TtDT}Bdx|Id3`>8Ye
z<blLfQg3zIj0vnVr<3oTOl~Dd!T5x<9m-yMFCsLqt_wqc7@KE);q%Bwas<C}$Zg67
z{uKZX-R0P&&WPzWY(HWLm%2<TXDD!4!#%sbV45*H?9)7D>TRUpV{~F6J*D1M`2zQ|
zjQ&}T4YtDzxkuE@O#D{YM>o@8XBZ-f#8j=T9;!41#AvPbH3(`U&n3Q=R-e2RRCiUq
z`M0Yma$CwT9kx3z71p)oHblq9q^D0z0o$GAV-)1eYz8RD_|3Eu8o!AU42Cwt8{jf%
zhS%N@RhBqGNF#SHDd?fFo&Qh}Bf3|eR2uoIAsSwob2=czhBiGGxn{u?0Y3(!F1jA3
zV;~0FjxflL<zF#&jo=$cn{R>`Rj0&Dcl*W-<FV-Sp9CR6y@~E=#$2c7IR32R`-m;i
z@(mk9&FN=pSysVcTJ!eDw$iFle6!jHKg&2t!@s>yYH%qpYe{n&`6}j>E_FMfUx`x>
zr-oMW9j94F-`5(tk<*u!GKTY`Nk*-yVlW*#DvO&y*91$Wq0Eknv7TXnq{W45x_lAs
zQdZNR@Lht`vuqq?%#wQXi$*RbkKY%VMK=$d1C_P>ptSB}_n~Tj=$Y<t2P#j~E#$lG
zfh`nJlBmKl@G;Ba){44-q?C;xA@!+F*Hi>W&)5-CblDQEAkKC=l<%W}D6iTq#)G$f
zExNHfDUH5Vq5Oi>aI#BTqE)$EJ@IWpzw}~Xm-i>-UOJNx41Ym}uvxw98#cJSKP$K5
zO$1}kH0o(_Q{{9<;%89Lmh&$@=*#9X@b`nsQm2@aEOp?eT94JGOfv8#m)l*+?fmTU
zKQ$bYCUpn;N{+|@LSzRV2&YSVl<+|N-zabK>AY%!_+Ebk`l+j@iy_D9NY0onQ6;id
z(c_I=KRfa@hH&hiQxXkgY~(;rwV`6S^uwiU4$vX&dMa-0sw<^_!9U6@f|b`WB~f3k
zF<NrVle4nKzGJf&$Bcb^X>{2l&7Gp2jt?XS=?ntRNeA#r@!~|Yi=0{qI|2?6jk)D1
zcV<aZ@Myw=bIb3_%EGKeJKLLOpQ_9T<!DfQSS#D5A81)Y{ep+nMRKyeEP;+k^`{$3
z$}0N$1|6=WHmAFkxzR#IyO)VE)jhxj-;K=@zaD-u9H*R)<}{9K7tG=Tr$PtjoXYof
z&!NG&^eg2<453rq=}+2C9{KZiavis~4&HE_vYmDbJ3t<D_*zP+9&krl;vJ;9)CIAs
z0e6V~ohmuyHmC9oue<cHia$iUKHN@pBMO@<+n1PVn`3xgb)2E9PwG_Z?Ch>Rj_%_I
zv56P${G15?6oR@*5_v5*dWF=p{1)moLdL7^g$&>jf0(ixBZtz!Cpiz@*I+Xglw?N^
z4|=aoI(YxtpPY1<l9uX`H3n)yWw%(+v#IPlI2*GfSkvA)%);&{q9CwO5c_zeoV=+f
zV)kUdnoi#rXoxagH$j-1IYp>!|A^GvhZKit(s(gk<J9xSqzGM^PIibCh>otKH<<o#
zK_K;EA@#v4<}be$)R$k<#eq!ED7u?gHh|Qmz;_YUZ=38gvrWVJfuieaVO38@8-h+{
zKW#85zF(O?KQYc!K}#K8I`2ehYvqnW+BGruOGid4+r`2_T4I8iu1IHgAKQB{WpQGH
zl<6^+kOckwEBDqO2<XT26uUwy(bo_oH#l_l95cIe72gYi3CL8>^r^M#E~xd=c|xZ;
z-O5xc!J^FEvztXc;tR%jM*fj%eu}pxuCoS=<5TKnrKDGNrcf0(+?En5{-=x{4X+PZ
zV}Kn2TY}Rr+Y*wFk*=1dIKImoOV^gF2L1L_I&dE!uo*G&qM@+|Y;jJz>Zd2D*QLl6
z#au)q8t_*uYO63$NKmgVsi)1!e~(l5@DfU_s`2VN(5YIKX>x`E5|hNrl5dWz=x&O6
zCs~S0^<{J-ailaLwYpE)`H_ozmYu(2seY5;ojb}-oh&;)Q_^iRr<%6+N%i5S(X{=S
zlp{-{eBX}@Up3Y&4VHiQeS6Y(F?I2kzKiKg`}$7x@a^bn+TLc`cvknbd@)=vV+ygc
zA#wuux2Bcvm;pUD^f(cGXbVxFEO-J80ezOHYg4Q;vMHvKV`*1R&NS#6Yx!xS;Z06g
z`FWaV$Q>r%aSi`;fzNFaLNt4j&kk=>*D+0jJ&324o$rJ<ZF=gjw6?!JRb$%TQ*M)w
z`o3N2`+8~Jw>^6Of(XoJ+5R&BEJZ#lH~MF_mVH`QcK)J2qs?zSW2%g&dqUdTEMDOQ
zvnU~Voc;l^udnpX5D|lPW`r0`OX=aRb#vc1J@*TAUj6KfV~g&Pcly4*OE-}E;cKzl
zT@KFxI&(#za8q`HxuAwkl`ryzr!y|)2im-@plU7*m3ph+3}Nx7=`!16z6*(xrz?q0
zMJBa}XRfK>iz-u7>coWPj83tiYZ?^M7A4|bLNCx-SBL$wwD=s=uwLcj%S*I~jDXy%
z?m<)@lX{e0PsiKVl7;AlE?x}>VNwY>2$SfJghF?A!f9paf0ah~E)JA>tM$5+d~_$=
zN3QSQEOIFqkmRcm`!4n<=@<UE>^#K4*T{y~<CT`z)kYWaGmN?pKEB~t$xz*=>dL!Q
z!#ez}?o)BlRCygT0wZ6kfu?z4PyU&eS5c14t%KCK(N#7+ONuXBqREm>a5e6NYqLa`
zCFy+i`m*sbb57-82bW`QdU|!V{AEQ@z9=SV$~Tjd7<<k9Ywez~5O>Af!EK(@*U3Hu
zk}&ZH;aIrXXL1Kp<hJVQilDTA7O-(SRBJ4ERQDKb5%puUIN4H87Jm~lliM5Ng*sKn
zk2p~La3y&n(B`zc9Pt61a+|B}Tb;aJc#faPF{<N3AIjORdaki-tNuAYIiU;Lm!_Ws
zxLuLQou<opG5l*|@hvnL&^TdKPLomj95sDQu8E#D0&k=*zeqPH?e-evy4iA5*(pu?
z7yS>GpR)LBe>k8d?U*Wk@7D(m$d_7vjx^Zf&*T}2X7XYy51wtBKZMRIx=dS><+k!~
z=>~Mt**e7_kJ>^-lIm0Mk-mQDY!lhgT3QU8RIfJ((mCJcg;2&;f2zlyn&VG>*q@rk
zw;Wq((H3gTd#(J_{ece`!Z7)_6yfhL>wKk{kQ(SnlO={W&bP_Z2Dy@Q_&Nrdo;J`&
z2ofvr^^jY)LNHaF>Pd5z<t$?bvn{U{#(rgb`gBkF)?3hf^Go-iJ?HItzuacpx`RH{
zvCp*ie9}RG>8=TfPRWh;_%lx@9Yio-ykk3ki}KL7NK(q{Z|?zDckJycrB&i}+;{BB
zCn{t#T$he-@->r1J!#@!1@mQu0-T!Lpf3{i(ixw-R`9ulLijor$-k1~veg1f`W(sf
zy03Jmt-_&}p0*&6X0}>iX{~<oVc8Z;u`Lsa(YML`2=wG~+R5iZ!OI)&FjWY=+Z}So
zvIbji6dfkaF)$%KSZ&Wn?PV;NZD-J=X=~5A7DKN(G(~402#s1W$7ix_wVcs7#CE!&
zaxutDADyiGO7FLA)&a-Pz^F-~e7b~vFgY_Q6`Ho%*2>cAw+uIC>>45|hZH$u*P(Ca
zj80vBKyR&U>FFzN)CVdI`TXM_>r12jnT=sH5@SlEee)W1VLkpvCui;z2VfRD+;$i}
zE!OF5%jArXq+=7S^@b|jy08bq(5D){?*`l&Y<u~kV2)o8nt*JcM6=b<A-eI*dpfsA
zS5NEQZ3c7|9sW&kgUPm9Q{~@j1?~dzwJAV1u+W`#P40Da#`-#?hu>C<9vE~x1EW&u
z`y`=WxpK5$+F*LO$+U4#a>kjGt7$M{8QvocG<**&ST^d8<@$Sv2i}m;c0<OQTp7p)
z3EAAbP}K0cKvsWEu0OiGlujvNy0*>adklRD-Ocv^2=Vu$|5L<OWJ|MT+xkFUe_$0D
zrM(QX4%5c{gxD3`NbSE2srYWB0uN|Zgnole$(j)V4553kP;J{*Wn1ejT?Lfx;wY`G
z#z<#&FxzToTgft3RcYjm)+&9UK&mF`;8|%|=y^>S@wNY@KCJrxv=600jWi&_E}Tyc
zDxa(*K6yjOKPlB<E4zZPO1H?iX20#YY&-0?HPgCpV-M4xgOd_}h2uwiNVd+XoM_I#
zvb0{Ut}k6Ao<k=jOM?RZAmqyE^aV}oTUzX{Ytid$o54GwvAS-~<!@tDR5lSRmPi$*
z%D4ETl+~bLN|gEuEsjMXP3Q?6)S(FSCGOwINoGg2(TudJjI~upvz3p9j1lS{C)yNi
z;F^+Fph6AjIdudn3Y`w$emP#fRJ<AquPSY%Yshvy+*8f~bz1xxEpkS)AK?s8>aahf
z8O1@xcodBC#Bd6Qj&e`?avJsm{-Hl{@on;T=ojm}x|yIBB!erL7WhgPS5ey)hKF@(
zCHtuP3;M;Q<WJ<C2hRCgG?R_mLoL1wz0q!IoSfM?e5XGn7^s*qn?9RR&Nusgb#U)$
z!%6;GheN}<xpAfLf1?TxL-9e=3QMk+GtTIa9hlda0%vd5wqm2qdCm6;F;;}dN)CqC
z*L|(m?F9CE&OC>SJ<>=e9s}kLwoQEVXVy0VtlID}6@|h7ISSD9szNF)mTjBl%(lAk
zdj-Y{Bp;)CKqep=rgs}n8xJIBo+-J~pV{_47mEXR=c9B@2i%)-{a1u<Oxk@zW?SkF
znP=d(X4D?~8ibp*t=CZqiYg^{10_g`zjRYqPpU#Rr`OCn%%~5CX=YbJ_ZDrdO;6Lk
zeV?sKixJb<R;$c6Py1hIKH@x<(FCh4a0XnuEwu302;*5~+h(7;NgyrTq7S5L^qg)%
z`s_3Rs02E$GGsquiXyE_0t^N;FBC;Y`6m2};w4>78se&duX-<OiISn}sJy26&l!WG
z;OCMPdiHDtdFYhQD&h^cCazN|G<p)4G00YKTd(Kmf||@^pKL(`e^kWrP6fMo!`96h
zxBcfRbTQ^^Vhe780&yx^XuH^=OIu#~FWQo`Mepzx-NaC-Fo9SgvzrC}5)x^xCL<`%
z+63R~P|I*gbOksir01;BB-_eD=OECk0^wgzsN$xVqBLOP#cG)L)5Aq-xHpA!)Ub)d
zGu80b6rQ4n`%~DehK&>+rG|-cpiV{;3ty|I44{-N)G&E8;TScXKt%nJyhHD`_XoBt
z<Zfc*jttE=cTSjX6}|&J`4Aj5z?HdH!3|iC@ZFDCg|`4_0PZ<fVHsdbj#Wr~*eaX{
zY|pj|`w@Qy@4s|gg_luo*<7nok9;QpM-f*5y^tq>ya`Bq9{KjNp+&UHJv5*H_2BRS
z{Qf&XOPL^y0`vuF0abv7fLy>#Kzf;)?|nS~3g`nDw<(gAwNwzM0LB8Y1DFANz?mh2
zaC(WF=gEbFumbQZU@Kq`;0WMHfPRr6!~uo@#sks;PCyRealkWxKLOSQMC5P6^AzA`
zfc9}g=nc3Qa2vo1umc_e%m-8eRsz-nHUa7Zxv1j-z*N9(fJDF*00Hp*0^kMM1$Y~<
z7Vr$92rvUM2{0CLdcGjsi02?cZ$KyFKLUb_)cyw1|JMO40aXAI(7aTA*R)s=jst!K
zTm+a)L4UwVzy!c_z{7xIz!JbRz+V7w0=5D60a^e*1EN9S?WkiIo_7K!0UiJp0LlQ*
z0oDUH0~!J8$U72{2<QtC0N+0WoB;0wUI+XYX)6E~fMP%{AOU5ac>V?NmjjjniU1zK
zOh7u|c0eNF3P3dAJZSwR;1uBa6KdbRa2aL*CX}o3VR$A0q5(fg_=&#hZ_CLOFAv`~
zGBpxTy6sZ>Z6hf>BF&bbnmJ`g*2KaBvA8hbV$Uvc=VwhW5Z%QE*<xN{L3Tdn`|ayA
zcgdq-VNup}cS)gCJjXqya87o<oANz4-CdA9&pj&^p4HGC%!~_j#Rb{L?yQWwImLza
zS%558UO`Ubf|9I>QgN}nKy<o`OVA9-!lgWF`)<)<TUeA|SnMv&nkMDVdDJo`TPm1C
ztFk}Fso%#Bs@+c4{7bx-M)*y<_OkEe9|lbyE&`uQm(rRsM){9n>@LgaCAmbt^_S&q
zN17e^W?q(WCD9)F$}USgjkIA%Ynp#4Z4Aa~VKH#JEUf}*o?_wO{{Ba=2>+v3{x8uh
zqBcXyPs;-}eL|W$-z~b+^YY!P!a*S&;+&pWTq3Fo!f~E5)x9v1u#d+l<`<T@h5kB+
zn<v`~^GL!4VT*RUJ3EJxFs?dRah_VSuu5~cTeKJF&CeFy_Ts`^q&dXmyn?xC;Wahi
z@4S@=Q!#JCBGEmg(3O|tp6JOg7CMCVVz+xr-Xq1?#fya1YVCz{Qgd>O-6bW^Q`#wo
z**TH-wuPu?Ddov1%#rflld=nPc;6nNG~2>FabjT(H9UuxU9NLw=ZWcs#SUPQ@4oL5
zuX~OtbZG9*FMK3BKQ%wUaE`ztF~R~IBS5dAQd`0NyyC)wc~FBY{w1kGP?ujK7SEYi
zg!XoJh4WH{Q39u-ii>bf7bR!p&2tOM(9iRVpk}*Mj%gk4?BY2dd$#CF6}AxG4o_jR
zNb#vq(5XTmrKL;x`InM}0&Pa&{K&}0Pzw<qot9UOE)^CpLY~>Y9Ay!K@*|Sy2GYV~
zlsYB5M6?wb7h+uR6($zDF*=u*ys8W5-;{Lf9Qkf{k#Le~m;og}u}~_2CY{sOjwA1J
zw@`X%G$<{C?*vMX;Mg@B6G4ljMQR`36KJry-uyzjBi83~yguVCyXO_D#1hQB*1~!7
zK<X)Z1*j*UFy}o33K+lXmvakB>{iFAXi-6`kU^!>@=6}1p>`By&mmfB)zQx^053c$
z2)amo3Xc^=Xf?xw2W)0;Bp$&V_3wg|%)`iBj<q9_OYP~Qu0C8woxL2Zd^eC1K2$Nx
z77?!F+_0!bbkDo2LWNShjq$y<=~Hb}l5QK3lMnmfDA)>e?AduVF585(;_L-8Am6FN
zDadwy{&Zq@feA#l{5%W=Ply#JxaZ~-L`ppat&ywJp35EP?BLLXoD6g@doE%7N6qAd
z`Pun-Ip~vG<1VzC{YbuBEhY#?PT7e<P(#$8JWnlGhI%5SI;D_nfId85{@lsvpdkER
zGr6RzhSOXZsWB4r@t5O3a#JXwSD$MaloUZ4#oSckIW5FyL05SfwFN<U&n*U%Mk*5Y
zT6<xM2nR)s)FeE02`80M!sC=eEkHIhyJaIgzi_TSyEuCua=ZizVTcMBMQZ(u^KOJ)
zZ3~4P1yW>Fd3Apg)Up!_7e<P=tJKdfDVbc5TbL?59BJcHUtiLyL(U68e-}q`UIE^k
zn};{Wyf`qar_oj$5N_2v=75h2W)!MJ3rlcGiuf??h>TK(yFgMYo^^@8C{ehZ`pzp?
z2k6c25m8I#2);}70x2VU7Bx&Pg!3m9Txu~g!WmjsH<jS+`<xrpNUAUfRC3RG6r(>u
z5=G+Vv0QVg14EV9^86F>#Ch3JvHgXK+4=dGGv-^}*g5k`@_?Q&LDjq<K+z+G*~O61
zd(<99;%WSUZK@M&^NPepm*r~L+*jl-xRkI~JEJ&z&ZG9iBB_WIMlcAeMMazg3#3$G
zc9-0DXP9+4C80Cta7*Rr=5Cd_`d!BF5xLYvIt&JxT#&<sJXN@vbCA8*JwH`g0}d)F
z$`<E%E=i!EwY%MqUW#WrY}30mKyS2~k6I$eO&H7h8I7q!1TI2+!lPV0-r1#@FlV^b
z`VEoP3B^*0rz=_bL&Tfts`pXe7x(<UIqr$sMI}PKT5qJ4yVE8|5c-qgm_(`$6fWk|
z6yV!K<M6oi^KA>=b6{Iig$>9Pu^}p}3yY94p&0T6+=|l+^K;x_8E7<~kF<Y!apAnl
zX;UB$WPR0iN0ECDgh9<tGaZp@NXI1Glr9DNl?F0oE0E?nv-2gie4;CUuN#{6EXAft
z?&3xKHTN#`Bz5T8rIZ7dOC=FXQPL+og;);>LNmowVntdxd?|&u(@Jq-xk&lcG-s;4
zi(rrpt}&@^4@EQ$e2PnHaD_6mOCE(Z-f>x~iV)_1_hn3+(QOaRu(9eJ`r=!5!5rj>
z(%hHt&Me3)$P+;=ve`ch_Yvj0<8G9ifhIEC^SB#SnD35M7$d;&T%r*1q7WImh(%Hd
zQ1fq6$vsFFoVMwX$@fiF(fj}VM_()GXBF<svI?h1S%oYl(V@Nh?&7@MMeyn@c|@iB
ze0TAkmZ3RAhenD`?n<?gGdveQ?IOz@(q0x%c8Mi78~w}~VIfDPu)s1WTPksvAm^NX
zDaRdyCp<nHdJ8la+=}cR3)~e;PBsl)j%A)ZCoh`{7l_$&M2qO5B9Z2<FR^4xBD_J1
zSZFDHM1+O5WLpZ{3oLWsIgN;k$$*7JOSy%`^Ek6xvc)@NJfc{1=PkDsl;o9+D9Igx
z$@wi)9g`gosSY3iYsu%+^WXFs{Twl3M0d9xZihz_31xEM(L-3vB?TqY2ytE!z0V>a
z22ZNm67eO~x)*>xR5O%ZQ6c$V7IOPXSTeF7bz4d#V7#bMD(0^(xm;E)c_kL^QH)T>
z<MZ)WVNbeM_%zijyf@V<JcZ{7_d>pIQ3$c2=TK?HP-$q%P$*hq*zXMY+<%D=_@h-Y
z0O;BIoK<K8v_zhJBhSr|=c?z_d}Vkp1b6^hfJp#VTXEDQo+xRV^!uiy*`}-ER7otH
z7#VkpPjNxONhUH&+{JLPvN7|?oIEAFV6H@N2!5$KUa3T!QTV9)62Yl&Qwj?om5REP
zy2XyvN5t>sg{Y3pwan#uh2C||7byK-{a#Az@+0}!-Z<h$dd55mI+u(aS`r&0+<4dG
zvEy&K_Rjx@68L}7PtX4^(2;OVY2|+*p0u~H9#2~R5Ay%Q@$z2}{+)|va%Ayr!;{Jz
zG(s8e)mb!ZJ!9~soku&KbbP?0`PX{%oG;6ePXD3jN<6)IZo;z=&&L0dU&Hga{F`*b
zr{mwG)71p?fB0`H)pX16q>uca^#AzZTdL{y-${S?chZY~C%x=<(wF~E`s&|F-}F1_
z!QV-z|BkcepjM#&$*AjB^)=$XFU3dxjJV-ay!pIV@I+mTTfspd{iXPNy-rXrYj1fE
zoiI#uDPC*P5uaSDucuDXUY5V5mrm%o4AW+#4z~HyuQ3+wT^1kNTL-`EQk?XlMNt1k
zQ4J4)zL)jyzw>KvZ#R7R-FH_WK79Dbk3Rb7uD9QQd&(Pcyx~~Ce!Y``!kag5zGugd
z9b=oDo3H!+`|soa&szHJ@1OttXU{Lc{L&H(2FI^ny?XXjPd&A0@e_+)SiE@tYo$x(
zzqPb%(K}`3i{5$i@r7^s9$)yHZ^_~ptE;P}b?esMziZd7J5QZDHGpvcKWRG@3dJ`z
zHs1d7%P;4YE`DO!;|q$nKKsn#<FBm`w7viS>hn8yz47brPu{t>XYZDaja%NnSoiwt
z7dEe6{d3J9s#~g;JpMuDlV!_*^FyD0`suB$t*w3kJ8ieLv<!OZop<hi^2sL`i;oq&
zyCSgY`?ubdFV@wqVf**jvgYPp?8KLQ*~ykBz(IEW-~raW_fvMLzL7P2u#J82#v2#c
zuXyI?>L=#!c>3w57rgi0d$!Z3Phb6CY5VBWqeEYP_0_ETqWEIj!os7k{bk9;_cs55
z?QVFD9r-NCTF~a{)1R~NzB|sgZQH_fa<bWL>(;VU$B(m9Cr+?t;IjAQdiKGVt?c!6
zHEh+g@=)cH$N&7dzx~a1;>3x;|5@8KUTfE`ePDrDvN}*&q|~f@?AH(8sb+gVUdN6c
z{*bkt+|R!I_81F=PO?3_x3j-IHJ%l^`mnKg-p)2|*ucJNX<=UhkIxSsVtX3u*?YCM
zZ0)*NFRpr~{3p5miPwo&e~%6{MzyuI=?kR!D=HQ|`puf>=3adF^(WZwk5;im``%_>
z9&2RZe0_w4loM=s;|Hv!I+ZCe>DYHK>R91)152Mcj^psvmtV3I$G%{P4mGj*-5;}!
zTi;=;UtP&w4m|P8(@P(JmB!-V@x#7-`$kq(RV^qjc=U^x{SW`T>D4^8YjZU_2)ci9
z=mXYr9POVvz&_dYAzQyJnT20~d$dx8HiQ@4AI<K#eKdRLt+&{T<6p3&$BwXlhdyQ7
z8iH*7<~P_YuRO#4QeE<MpxVEb+Pb{$ufP7fe@#tIR*^UVo#jttcdmc&0k-}1`E1{N
z&#~q`Z?Y4I-eWCCgY28H_OZIRA7Y_R_b~Rhov}3|82dvsW6uaoc}`%@EQn<<KL1B{
z^yug8^DjPU`#w9sg8LfS-*?oqS2w=G{`}%n_S};E!!@t2$)+~D+HYuR7*oEq?CJS)
zANeNmw1d6*$|J0PV<|hdZ3R2FX9N4P>23Da=O41uC-<<gj(x!D-h7&|18W)k<VD7|
zJj&RgZ)4>T^kdI0n~%1SurE#=V~2pl?!yPzwtbE4&4v%z+V@{)ue@5#Ry{TM{4<qH
z{`B$3AJZ3qg>X3B4}9S&el-6bvC!FB_3Tvk#@bxAbIX(LK=65XtZ^M{*|(9MI$X<6
zAFpTMocx4+@a77}PQ1_9kvAD@KzMT=E17vct9t4wcKpN_?6WVL+1}3&vATo%*yc~`
z*&BPdvvt8Y*{g3n$6kImpFO{1?q`4d>t7!vJ$dZdu_4bq^Nc7xGUvqH`S-FXU&vsq
z|2l`&ZY^WG>sGKM4Qtq!pT5pc9oWoHe_qE<eX*0hzu{TNzT5`qcoW)x9yFl#hp~zZ
zA3N0i8QXK@0IS2;Z{5F}y|H%(du`AAY~7B(vzkpS*q>LHu$RhbwZHg>>MD};y?giG
zR$jU^Q1r;GA9I&F*psWW*q>hWvNyMuvJXFchV8EZ6Z>rMdW^wF_BHt6>%-gFmcNy=
z(7_GtyH8(Z%C;9+=uL?&lP0oP*VnL5K0nAlJp2iJ`vCgAua3R8_dWKv#!YP9_O-0$
z?Pu95e-_y)-^`1DSW>isbTmP!wD9Gk!iUbzT{WAPZ}PI0A1q?8?W|(&VthZ^w~FmP
zu#O$r|98m2yX>ojTfh^4XUCg1vCo0i;e#95r}b~K;Lgo#`==kXtp^&}2DH0=Pc8e~
z?zh=r>)&8&KYA78{}QWtOJ=XES-@6R-OrwrW*^+LWy@r;Ev3cMnxZFj&wJh~U`y&2
zuoZhMS<U|E*jt~iVecPZ&w^iUU=3evV*5|L%Z?m>j~zb@c{unkJKD5`H67l<g7xnM
z*C59E1GaweyKMa@o7uXZud#J?Xm|U|X!`}Ww)R<8vl;ELL;K+I7YnDJ-mqcA^zGZX
zTbC4-)D%<~T=0Cbm_6~apDo|JlC1`RZvcP2`^CHL!~fOZTgPR!bbaFsu>}(oMZ`cw
zQ4w40P838@B&DRgyStl<?(Xhx>F(|lBn0N23-CUkbDwiR&+~qs-ygrv=bg<kd#<%+
zX3g3&>pN>-d(YLJg0kB(P(gbRDrwI}<sEsb3ZK_BW}u>mL=;@;huic=fyF*3FxL(F
zr`sX_G+PvqW`q2bEs=kcDe{Xq#MfUP+W}$ZqWc0l3Edqe+Cw6qYe>p?i5rXj6ik%C
z?W-fxVk6{M?tlVu|D);yQF2o_%4~^3xh;{Xq%|5<Hix6e#z53w?|~{B{BiwG$Ro-E
zc?KKfXn?#zu>L}E4ADVeLF#y1C?hXldF1IOh1?x@kh8`UWXH#}+&*#5pe`-vE~z0j
z&mS#~lrzPUX|6nSE!IQ9<u)j$+6`sa2cVLs2;6QoYHY^#4PTSiIzLoj=Z{M3yir7g
zCvuN=LGIx;$lcErdH5J0Pd|Meb&;o!I`Z_uZM#Y!4;K;SX2pe^l^!4){s;X;{Sz@>
zQ$g8LO6KE?Kp-npN&0}yGlh^_z6y#gvqqV<{^(m%9BODw$M}<hF)j(UHAbVR20Zo}
zf>3#*KT1yaMuAbD$RnD7+~b{*M~Drc!{*4-*9dufYa<VW0`jmELGD(3h@j7koTZqM
z<yYo*;+m<ds*27~OV>#5>*sNip!Y~EnG0EFNh9APQ<PK{fGXSaQA>9<>gs7gogLMv
zrLhn-)#ak<nlw~em4J%y`1|%P1?3kfqpXr-lvJ9GVu};7&5gl22||PjA4CXtK`x#q
z$k|B^IhcqcE5(n<NJ+dfJ3IRZ(GEL1I*BNL`Pw8Gth1z<=YVYRd=IQiMupw=sAHrb
z^-YYR!HH2cI5vtphx$-`UmL3KZ9$b7iz|8>QSV?s8Xg)#7}x%YBO|D%e*{%Fjib`~
zF_c|3g5pYtkVj5CG7HXFw6t?^Cax7HCML+^<Kv$yiONJPn`zE#<`R%|X(S$V#i)6>
z7xhn#qtTfuG(Izp#-_fb0W3r7U=L~-=tMO{Ir`gB|L`Ch!R-;_=;-KgaSZhjjG>z5
zag<j%iX!s*kZnR0(y$8}_VEi4`L#~1si`?-X=1J>D=5>Woanq{Q5=rqo6Atg_z)^<
z>_GuhB`E(}9b)~cgn}YckgIP3l2EckhIRo+MBW@l#%3T+UUgJjR*lrO+>wQKAW~9u
zKo(ZMXncGe^$m=pqN-s$AKQ?=cgB*InR7bEpBumM56itHE-vnwypT+^o`dazd42?*
zccrLzVhoj3^&>Nf5+o`ghA!UJL5^-|NKPXLNvee-EB9=4ict@LmW~*iv{80$HG0JC
zi2Q>K5to27VtSyk-2T|uIBIDhKtAzxNX0tjyStaKEWYjse_sO-*JcD4S0TACij9Ur
zehWU8*{E%}AJsMuBU8H?B&}A8JOY}KUw9)*FK9;&UNy+YzXrJl)uNQFcEtHL7ZsOv
zqrk9cJO>+)fkhee3H-L)KJmIVx3wdiz>Fnr6WgHDvNDFhuFJoF|Goud(M3HqeH$%R
zlR-jC>{4Q587gh*LH-e~C^5YYRn`rljQma%lhTI#Bb!lHaVNGlO{k=z7dd-1V%so?
zoIM**dUhA`3T(o1SK_|oG27LJ!csC9mCPJ!;}a5I{L^}zcpZ|Gk{)R)>UgTFnhlx8
zMl43um7tQg4%9v{h<b*GQD^@k>KYtGEnNesxw{{=^$nuI;ZfAxgU8?KI38~+=bc@{
zs2`92o}ONmlv}i@Z06MLg~!}~wGQv^@85%Ec&w?U=PW9s+iegQy697py_DTqi|Plu
zP|xTf>K`4(^8n9D9EWiK#;}g@JX(#!>piwS2RpmEQFKPpf|eDb+Q&ceE76zwPi_CM
z2jaDGa&VCr7uL#=(z6@ajtX9MDoH}oO?WO~yHJVeUEN?8o})czXmn-Xjf@Oqzn~8_
z;Pq!}PSKLJXXLn!xubt#Qu6bjo}T^xowk34si~>WSl%PCF>$Yr46H4rWsM7@4D3gx
zeSGHC62ceta}t&;O4F8Hs&Y_SL&H*Kb;D9#QN?0#bo#uJo$ru_u|ul6m!E8TdHH!P
z+yABKyP}KQ+S*gGvGK2MtX(ve)UCoLRIQ6eHSFre^j%w|OuX8(&3#+6jXmns4V{Y&
z%$+>Fe1pXD3kt6PPy76z!sO&68FB0oeQ+XX5pk1<-Nf|+Uc=lbuA_)+x#s3(n*WKv
zi8M*k5`e}6!0lOp!{Y$k+5t9X0FV-+CIIo5BS`*ZC4UosmCIO(q{KRjbrahlwuOkd
zcQZ&4@jeC78Q}(EBP%9BbMtQ%k-)!Nzf4aKH!qOGt*caUhK>#{F*3s4%jCfF^Z-1#
zdl@K?umR0Ub~tfP0<Pav0zvsFAS}!V{BrL>M*1x%ihcn_1tCyTPyjPd3UE{14n9g-
zAWDq{>^U!hso+U)6ub(~(zhT_g#>KBJOF2<hoGnQ37iz)gN~R4m??e*J9Qz@5?262
zd1Vkbcn)ed&q2ue0|+^=gSyiP5Ds|^O7Sm1Fzf@!Cw&4rJz)@V7X)=9MG*AGsRtj3
zL<@pak`V9)%7Ac^94Mu$gJPBfBp8xGjNu-L@M44-C#)oMQW$nz1M_wyfUI@_az6n}
zgp2Sk@CGdTUI$&X7vQ4x3iNfE!PS5jTpT`tuiYnzbmsyK&(C0#{0>}<1i;l+5X?1I
z!CqehT<sLW%Sjl#P36GTNdYVZM8M5Y1awom!QD>@%#x&m5GfCVeu5C?`xU%=6d^HC
z9s;6OAU;|Zvf@9(bl3w}3gd#(Xc4GS6o#x|MJP&8f~6Qf(9IGBo6N6ZoTCU%xhmk3
zrvz0Q%5e6Y6+CA(2aYesASq)C&pxEX2i{;1m5%~(r6Mp;Fami~N05&*2RVx%PzVeF
zu}n=+%rgSj90yR(iUk$DGBDE92Wul^+*f1J*LMI%Q#)|6w+CN)Bk*%}z<n_RSATnO
zi*W?c00T$}H3r{!V@QZM0dGGCNC~n9?*s>kk9Pzk`(W@U_=8PI7z70PLtJnm=Jkf8
zIDha7jDvu<I7m!Lfc!XP$WOL|s#Hf<Ow@*~kYLD33xm9*B$!F^0i!}ga4t3i{Xz$D
zC~*Xr3McR@HiUqD0>l>CLO_KhBv!b9LB228mIeT!(iaR162PH29^A{*!7tYz(n>tR
zzuFfPYJDJ}FdpJ^Qoz3|7LpocAvenbDzlwoDOVqsid>+h(G?1E{h+QW0P^bspuRp3
z@(W_2p)48->tmt5HUZj8L!rIa3zn*aq5WGV%oL_TM@<4OH77uEdoq~VR)SsdHwcZZ
zf|%rX$Vkh8^7Jfl&1i+-;!KDy%7vhsJcz3)gw%pYh;OZcl(tGpZLfm({65Ib&x6Y1
zV#up5g1Xu~$StaY;@T>xZ>WRLrd*h5D20xeMwr263fl{yx}yLJyBeUTry082i=d~w
z4rV%P;9G4!H1u{u|Ii@xkB`9k_!#sLj>FLR2^gOk|DT$k{}dYj+nnxB*4BpjFvh{@
zzbe+?WT<CiZReU>Sm^8HY(p&aAGvk)%<XauOG^6sC;A&38~uEojg9`Xgp;8zmqkWl
zQGfr?)b!NQP;YN9E@5nJ<o>snTkEo!Tj9zlrlzN-3w3mRG5q!R`uX7|jQ@t+@C^&M
zRc>C%#OU<gbkAYZwa0tsMn{Kw8{D1o9Q$j>8+6~Yuv%pluCVvB+}KUBPM27qx3|Zg
zV94^<0)}s1vkGP877b60{+RA(zH$5Z?lr#(G`Kq%zBBq0yYZXX&$&Hv^NRXMr<d99
z+_}AX{W=}ohoQl~R5xoqmf!o}u5<JmC%0#AQUCDNDm$?NQKZ?Sp}w9}7i+zD&c8`R
z^Yl5lkXK$&<?!%S#)B^p@7*PqVA`;&VQ6S*uqQRe$%N%y&%YFZ?C`S}LLOd4MU|Dq
zQ+$GgSy^8n-o15j_f4lEB7a{?ii<VN>wih4yW`1oPRw6gN^D@d(B|8>>@5C=6dRpJ
zM`wxbEh%nhCa>SE>d5K%_D9b+g}l7-N=u0X71>l(earU#x*zjn_Q8R+=H?U^6PBl|
z4H)bofAs7#ucudLDY3wm?=pWDKjz264ED9PG^Y^xU$69_;rMoP+Lxbstuiz7O3QHr
zz8=f<V}5Ha43-90pWtH3_4=vLN^iHGJpJ;61aAf|P+ne9nQQao$4G_0H|16Wt{pGN
zaP<jhmTb(_%NNaf$F`Ff-*fPUhi4?_<yBTz<_bwlN(u`Iuv+)@^|dw=Yfo??@;`mI
zto$9P7%qJh4-F5G%+Jp+Dk(3otSrJ)pb?8g>?!6>aIv+u{K)*2+K<S;^ZXSNvB1!X
z<mBZ1{Nj>++(Z#BkXnfQ8BgT4#ldo!pV$F|otLj%4G0KHO2+G=bi7XLFTwnUg(=Mm
z2}JHNmoOqb=BGM9+)vQ>++}r*px~IKG|W9t3=_nV91@n45MhZqLou@@&&LnXs1B^w
zuYNTkC?sj6f#T}?>Vkrt{CrbOOLKf!@|b_*!N-T^&#3kh$DA>i=bBhRU_?X`u>(Zz
zf}DbaL^Cr}OC$5;@R92yD;M)qs(r4j{k<e2CLS6QnVer;T~L`rWY14@B^EHj++5~d
zY^<!z%ugxzxi9Pg)LD9VQLIoR`|vPU4{o6#-_FSlmoUMBjf;($mHFx81BZzEbl7o%
z?#wGuiBQarSBQ9lJylSG`CVOIToPPda2u9-T&&E`pHfjS>)-jvIYul`Xj1;f_vz{R
ziHVYl{_5i5KJ20N^%b|YxDg!P98C@NSl;ef{-(R<_yu*1sOYq|$=UhkFpYh>$;sKd
z)v&lQgF|<BQ<J0H%6Q(T5)_rz+J)H{7Z;c2XQyXz3C#bK80LO1EUfa|r!Mn5A6M3l
zN^ASR%)Yp^I6sTS_gTzIJTN=40OnV-|1}?XX-37hV*aLqMMqh}cY?B;s&Z<Btivcl
zpWvY1u!u$KZmLyvTjh6FQjUslom`w(t!*INBs(BY<|ea6)_#-hEEyR=VUx_y#YN0t
zqc-qs!W-yB#kS5aF4oj&OGzutC@8cmO3BwsDU8Z14k&Dq#xgC=)T$Exu%1f#j`3ZK
zi%m6(inZGFGflPHwe59s+6yyvHQI^;#6Gmw*ux*eK}jvHV|Jmwrlz~TcBZDLy}Pcy
zTf1gXTf4ixrUol%)NNHcza-LkYVMfqZmM6Hsh_QHnyG8*o~f%}sIQx;*RI3tb@qRO
z83TRXLi-F+cld8~X6EO@a%P5@j5eq`!e4@ctbJU^udBN7^S7`tI`G%pm*>5#z1DJx
zdCWeyF!zi52Ns6#@9fyBE6J)k#>IC{exIM6Ul|Qpl%aZpeb3*q6V;@ztfm$h*V@|A
z(KU(18Xaw{bNKtpS3(aip{k~)rKJ_yIy5j)N3gdi^!+nCF%ayPm6cU-u*bpSZ=v_!
zLJJN91HJz(-v1(?<v>iAR#Q!q<rF(p8pISYlMWNpcI;>oQ%&r25!2<u087MF<BxQK
znBIn!@0bs-rrVaEW%wiA0MPz>O1k=P&OfCj|4Zp_^|t?>Zump4U+FKofAwQwHC<Zm
zA6o4%Q6Iz%!WEOO>S<X&xZt)`Jsw`sE3D{ucH2t2qW3?4pT6p+{`P(U?|$mFtCv9H
z?M6^wUk?)Rcfh0jOd#`t7MLC=V4w6k2xzc?oXjU+{ip<-yz21f3lAuX3WB0EKWKg2
z1?CFm;4H-i1c?`5Dt8~8RUd&p_F+9`nZZGo5Bo{je^ciML03)?3Va8u*vFL8`wEgK
zk{}c*4g&FVpqwcNs@clmrhf|pOs_5bk^S~0Fz<l-L|_2qNe?c%FF?=cC72nofU7<m
zXuEQNn-v#$8{dThhmYWFC;)Eu0$^jP1g=hs;7#-y&6UB&MFE@xzk*i8S8(@M0LNq{
z@b&$IeO6)c#J*xukRtXK)gUv93kD*d0g7UVyl^!@F~TqqCI{x3LSULJ4-Pr<aEeh6
zF5c9JN6e1ECEyGq^5!6+WCzMBw%9KW14*@Tkkg0(&ab&3tyT(piu#~xp$*DL1dt2X
z0p&zzkoENi^@MOx$u$BaT^+DC)de>PeK0X_0DVV0aJF{<ZyO!(vNHh>f*JTa+e3(x
z4R|=X;=D72xjF*D+XUP~9l$--8T*X-5FMls-Z3VS6mJBc!PXEKY6D*0E)eKJ0IzU&
zNC>tEpF}$_v=0Dl$3QT2jR1Q>5*UUAfrDo#Xh#KuTckfY`^JH3Yy=QeV!_AP1A=^f
zz$?HT`<gxw<{toI!66VG90Y!ez7Ub%3CYP`;1duDQDLzV5|sckQBjZ-8x1MAd~|#o
zB*Z2_3idfO!weuhMjwh(9H1)27Wz}{Au~7_3Sy(MpPB-u*cWvy(gV$0XRyTns9U)M
z_~#jeUy&okmpVXjxedfryMaY<IM|c~fJs3-IF-a=KQsXQpnedCeab-mS$s(n1lA-&
zQd1%nW;;Mdh8>^+JIKuRgrZzu$j%9d!u&w&hXz7Utv}S&1wlr343y?YLSb<XWaK45
zPH{4n;8;}_1x1Z9P+t=dU8Nz=UE>F+Dsb5+MNRQwU{MBU4kcjao(-mURbb?w0XBgd
z;NnvUwh2|>8&m?paXFBXlnLI!P2d;X3^6HfkeJ>DE>$@YT$%}qg?ZppkqxnR#o&?C
z4vBf)ke{CeHHAfxQ&R*Djk%CtT#fzIN~o)^h0f+Y=xA<+qV^o9XwQYbt~#jgX@s7R
zvSt4?r>GN(uzy<D)CW~f<IvRKi+$4}=*52O(C|2nEc>Vv|7NfMJHgi2z`(%J(Dv_%
zqC)bPDcRW`?q**V{~cEOdF7L``-dkdu{-GL{#E$TR8yX7vZ?)(-@iAI8;pz$_1GB-
zn*ByLyoAYxli%m^j;$pzBQiJ|{KjB=iSe#${_ywjdB<pJ*Q~<~y*)MxSO3Lu>EdnY
z{KBEhyyLVdPo5+tu^ky2%rO_g`b)HZ40j1>g{71CA3QvL;sosuexk1wZOkjMOm^CT
zmf;a0t+aG<_)$sX<5Sx&4dL?bZRX+^?UsvgK6BfJNH3jKE-OoXyq=p_yuD3dj&Zqy
z{Tt6*zZ;)c^ap)#u(Q3REkf?{MNa_7t((qWzbCuQAYD}C_JqP{u(Pu*I@(<38oe@B
z%{Iy#O#XrC=~%+F(xQy4%yuF*!dyk>COz(gAr<ve@1WqQ^n!xI(o#Hh+F~#@LPdp0
zKY;0nk8t_=#m2?OjgF3ujTJ;hL<C@>zl_YiD|CC8=|{c%BBP@4T0A48I73B7M(*A{
z8SG}>WF-Eej0x2dnrFCy^c8ZtrKFq`G2Faz<@|13-fY{!Lr<RvrWcG24;N=-WLTP;
zOUj8$NnN|bNWXI*Rv)H6_m3Kz#B1xZe%wfIZcI$Hm63$_)r)(GPlWCN<P#b@K0m)O
zKTq^$me*-N=Vzv;ySu8|F<q41C%hAr7Z&E{W+x`*SC`u}(_LLF@0fnz2<e<3ub<vx
zqx(jcxt^@|quN`wpFgL&s&rSb?-xv;u4H+s@s73Pg~rQ)S9Py9EdHEnT75+oK4^B#
z&s4r|Y?`X6shMJ%YHIvex41Y`ZL>;u<kRo`(bO_M)!a1II9)SS)6!V^bEIDDUjxvL
zPp@-&Zhm&|$1HJq|C87S;)|MpjL^5*9pe-8^K<h*@uGb3=iEqx?w@A>-$(86&T;G@
zV2-(=9*z3HPM`+0kDoci!di!h25i)H|4LmBR;sF>)z!7M9R8O2_hEUtFuv{g<=MaP
ztN-=hddBKK^8XYw{<wes>mK@F@1tuhw>7?DxoP4$h4^I$HT<zox%2zo``^|n!rV_m
zT#yA+CD=e*ofEuNPk^Ha8)&MDfs{E1h`LFDn7brY=&ykm+Z}*hPlBr<3lK2P&XpYq
zw%kCl76f+-Ngz1NgD2)S5BUmuX<xw8PX(fV<e(~91yG7OpfnB8$x{JgaT^d<@Bjr3
zA5hl`0(IjMaI(+=14{#3pCQ=UyMd3hDfkjF7W<onSD+roV?*$XHH5@y3kdYbxE*K<
z!NHE;5n&BJarWSsVgue01TZoU0!w=jup=abp>F^fBzQxNpAW=`c|&AqDEP#CKxk|j
z1g8f;R742Gq=$lMSPHH;5|ZNLATl}yBI46A-eRna)`z@g6Ua{S#yA@Qx$z-TkdO!l
z`34Z3X$&DHmf)8`0Pk{JFe&r~=e#rs$n(b-8w#lfo)A#w35oUo5Lz6GaVZ6o8>65(
z(*+7MeW0+!6SA{|;ahGf6qbcUO<n+07KK1_aR4;e`$A1~IAnf{fu=Hyq0Ny{(3%X@
zbvR!U4V6`CP*a@>^>ryw(VPaYjp@))9tvHxsel?|@fxHGtRo5`Fg^qPLz}=qyafWn
zn!!J^5n|FBAS$U9;?mo2?7*^RKu~Eu#1&>jLRJB!<rG19Lm9-?6hl&FA*2*_K}y*W
zl;&nYL4F<-6y!lwX%<wL6hLNG8I<FgTU-J8rBzT>Sq`-o6;NJT2jz9uP}M-h)?Dap
zD}$`|3MlDlf|8yVsOf8k_Ksrc>8Xd7)()s^9fy{IE~xMChW4Re=<OSXu7M%w86JY6
zp)nX79)V#T`zA+WWOM~@|6llD3udOjW$w^(i<`f^et$K4{H(agvo&^h_gCJjIJ*Cf
zGbh;=5<9C~v^aZn+Y@1@1C-?J%p7#i-N069`%`{d9=U_04whO>80XgRdCeupC-eEd
z;U_kRoffe9)dvm{DM_J^@7`QIv-1UPI`o$PtDwMp2D;Pxc5i(S8}~9xxl4=k-o4Fu
ziekM99Aj|_3G;RH4h|0XGFAY1e2t)X_zDg6r6W|KM#Nu%K4GA#tcDL2`C!Fm0WWL$
z1Ox;Ihx+}p=8vwj>JvP5AN|%vYwOk9D@)iX9LIiRJN6-oJ3<ln^;?ecm$>>lk%!1j
zEVFcb#UK2`&Qh=F?{|Oj9|G|^T;g-4&d!c9wzif!b~aWz?k+A;DJd!Uh|gI6Q#s=I
z;YmqJ4-EA*eB258U0GQ{^Vk+F;rX~!5TEcfgy7O^Z)y}k{66@v&sGqhqxSRl;WE~g
zs=+pN3EK~Z=O4nh4PpC_@VrHJIAR-!94!o6!a{;Lf3JgBr=F5XHMZwV*iIolrxCVe
z2x9`m*nlvOAUyvOwg(8?SLA4>){&8(zWgo>u}?Z`s_qywmat7l*rp+D7ZAn;gmD33
zOhFiTP+?&{!gd2;TZFKEM~)_{QN*(8>FIZ*#5lSz-XLs`5w?*C+d718C&Km)VO&8N
zzYw-JD3jQJaR$P69yuEePiCa0KKAkO;ulf*G>35sVY`j6?M2v*B5ZRJwwnmsWQ4H<
zVS9(LokrMxBaA5s<JywHi?ybkqq8K&!3Avh5w__F+i`^LF{)}#N7x1<Y^QOXkqFy<
zgz*4jEJ7Ho5XMI2X`)2%aB`LqA&4(v>_ZqU5XJ<AZ92j>9${OLFjgRpV+dm$!dQne
zZX%4M2;(erH;{7=^zq{nH)Ws0n2Ipwpz@Y9gzYE7b{}C(Ko~y|#xaC34PgvK7&8&Z
zVuW!V`8b%F78Vv>QPfuK#2AIJ9Y)xWB5XquwzUY`VuWq)^0OkuXLN>!M-jHi2-|#Q
z8D21-nx4swm$=(Zb&VY{mM&rYjWC8FjNb@jEW+50FpeXPy9i@B!q~q23=GB%Wat!J
zh_4^<^%>%`%?w)FCN&sWmN4!hj7bP%5W<**FdiX{QwU=e!q|mn7)E|EX;ZlE*T0Y9
z_}GLu>Uvg<7_XKvRv?TI2;&4Az;X|dj3JB_2xA9|P0#!89~LG1chCKomX<LZ7(4iA
zxOom^yk5juw}f$N3FFXGYIfnQi%(cVT2{`>|MboPQ6KsFMNFQ)5%LaRp{A~WkrqCI
z(K;#V*>C#$`~PkJ679wB_T|3{%eDr`cG8uyf4=XuVqkuMPv*}LwrH*zEzMQ?rAa&m
z>_y;|_{r)GL4;Nk;<0j%>z~Bgpr9bRa%Mri!-%uLKEQd&jS9=*S6<<-dBc^w<gdH|
zKHu=?Jivd>|CRqw^%?%9J;jy$2I5D={7M1LuAsS+-$<-+qoLui{I*#`Lkr@vSrZHm
zH8I_AB^a#c|D^}XjR?(d_|-uLMIdr%Dr{W2J&1?^gv)N&_(OmgB$fdQGyEx!ng5h0
zGW;ozS^mGy|0eIM2rB{5(gu_<OM)^a0cB8wMg}a$+bfZKHU7%4_%1lYU!&%#hk^YQ
z66~LlEUnybB|)?QAua&vpYe2MoM-$t5dXKw`RY&tgVpP8u%Z}E!(W<Oz0Mm~^|h=?
ziT{iAm%dl^3@dKopCZwH`|rdIQsUN#e{|x>TDY@e`RokIuXWtA;UaknELf8)AO!p%
ziVtddfdBvg4y-0<F>i)<Pzhu}{MAo{wBXNygt;5na1*w{0O`to@!ta(05gCZ*b`eM
z?zWG#|3oJRk~=XV4)U-W#{aRr%yRkV9~k&cd03_sdv+5>|AxL{nf?gVkHW%l_5Vpf
z8;dkoUg3LA()vHt5AFf89~1%uY+Zgg^sn+0^;5q}C;12Z4w4o6n!lm1g*A7!!P(_2
zH~%;NBhptS&jgCU(eHLx!@LeM;VuaYZ21Q|Ub6!$rjz|W{SI;e3nG2@-_T*X{uH=J
zvJv|Kp8nw%eeFM#C;oqdug~Uxpl^nCu#aRjkpBaH8<uhx$#&TKx8rLK^BLUsZlHi|
zt8|3^xPpJ3Co2}^e?CuO`Sslccpl(!u=d}tBME5Yes0D!Zvo=(dx_yO%?Gl$78&3I
zNstB!T%R<qUj`o+f#EN`H2v2&-~6}ZeBB+bI{^DJT^xwN#Kr<jxSkgvjQ`658&3H_
z;kSB!?{~q*mG_bX{NeQfu-|x0GoQrmD&oGa|6Bb7zJ8mR^}k{||E&K%>e#$o#|v1g
zgBiEO4?I|Ue%z`8J`=|MAf5@~GtuQwS@3^Zki*)eUhXF~{#*ice!)3$EG7N&@kc8g
zVEgtJd?3DIOU$j@`y#%dyB4UaS7^kyao6MA3XS+KFDaZqzna5lV$Etf>gD%%*R66=
zFTWK`0yl3fiiuLouWVt#M00`uJPkF!IG@B9VR1nwns=;^&R(UVCT^?og-29EoS%tC
ziC>21_N|*dGBW&PyrN3fxVX3s6OEj-_;ndRA$~C)nX_WQwoW|DCn0v7M@EcZ{sIlP
z7>~HH0Kd$yZJk!y`lF(!U--pkiMvkxE(`IXp_W|P13*Iht~8&Juq;2HteiBK<ipj=
zG}Ocm9+_xf$%%>*8>A<0)j-@vL5QE{3k|ibk|ZW72#a52pgDKz<`;e$K51b|;w}=m
zewCnJ-S~ofWoH{|c?NpM%Qw&c&Wg(s_r<|lQTpRt_U@|Of765Cn_U&~U%N@tuWq75
zzp|Ca+10%)WoZ8B_vQaiza`~(MTPmE^D8ayjDQPWx^{{8GUEkCMm|1)3tt3is6|(I
zUSOg*O+$@)f9`KYKlgjr&i(4hE%<-o|IcA8EwuI5_$ChTPdfnmr~xjm1<lVZD@-9H
z&b8|vtPchEeV>FqYgjmE^8)rW-*4G<uWgLC+BLDew2bierCU$zwU4SMH^_Zd;thvK
zwxwNY793n4PuN&R$1!A<{pORb)~>#bdtWEN^LkK5cG#oTQQB({ZQcZ<z|!I1W1EdM
z`eM@F91WQ}EPXDYT}j=Ha!01DJcp|FfkcvhY<f%_39JvSj@dAW=`$x((Z1di@MD7H
zSXIFLvwfyvDspByKBpJx%X!3D`)a<dOBHGrIB=v?;K4zwaMv?kTi=L1qO9;t;1$)*
zOp>J2DBgZLNq;jB$DW(R8gN{em4n^t>RT08LH>ER3&->XEZ2H5Pk0<^jwk75OuMW+
zZ@BdNDq-!-<M~G97LnESUOjdtCRFBa?-PV2>}L%@ar4~#+8u>)wkJmf-O{wV*D$eI
zaZ^3s^m2{)`SdQg?UMK2jeL0aa)D{Li`x?lQO*b1k4}oT=hhkKEAII^W)yjCaKn-C
z$|gpNuzmZ*B5t!D3}a0t#7IxI^AAlh$a`)M*_vk*$fiBt;5s2=c1Pl1eOZvm2VKg+
zyRSmr+6(jjE*H3P9#)b#5oB6=%xA-azUXb_dwnIeXvVV~H#pI_hj@GFSNu4uzQswM
zUO(x8Jv#@5YQmdRhTKX6L8kkyR0bpOOa*sbICg5VIj>*)ehu^UZJ9x{p)HI-iSEVp
z-Oma}uRJ67+n2wIc4LrS<Ob0|f%yq)rjgBTO)Mngiq+c<D?U<;guEI(+_o;_{_U=V
z^dcfDT)tt`FH>q&ugvL@x!t%ESMf=QkTUHgvc@a&JGX=NSt-+9A5{E0LPV*GCO>UE
zf3i4wuRlj>Rnlxj`Z>=z63wf79Ud#v*@l*F)9-bW5iBO59Z`DJ=4@YP5yx*IcrL5c
z;&zP`N6zV<C)cLJsE;~CW{w%?8&dOL3*KEh($0Qdym;Z;P3ue@o^N~U9@}T_xVe5y
zl}hkgixbkj&A28)Kik+|e7W%U#Ea7kpU<dWmzs2!yL!ZmqIc+YH(fdz+XW3pDI+~-
zu_1q8vAZB9eBVWFs}nt2F5DiU<_Zn-%6)F+`Z)b&*4eac-k*Pbvz$0$uAL&P^Q|tA
zGNM_@<V9?_z1Ef5M2GP^EJ!Z6x`9&i-q8(F$LSUvzizN{xLfaVK>ipXE1{{~-rNz<
zdVce(lpE*W^;06gh4U+SH18mN#X8n`GOw*>;UuK7@@tJL=`UYU*7<RVyaZ7(ZxG}<
z6Bl(kHBYN_bK@<ASxue=3w!(dKxb~(_cLWS46+Mnj;|k}Rq;9<u6y)b`us(UmaS#o
zKF{)t)HOC6?iwQRIeTcMdh_8nfBwvSCdv22A}X)ONN;%PQT0K8-=L!76Dk%y_5~>i
z^=-1*LK{vS9wiqW(=V&wxX#AA*>BrABhQ50)qz2(+Qp(d!JkA3q@<T?JO?!G?%$&0
z+7kB?9T5Kt#s_~HjuZCglCXDXcwO?~6tX2b_h3x5cY4$Q2Ww6tmeSe^iX}H5SMA+D
zHYtaQ#Eh_RcRNue^)2@?UBM8|!==0P(d-t|Yd2|1To>icWnI@CK)Yizhr7j_#1#JV
z+uh+dTBF97L^l<`%ei{P{AIkoqY7Qg@J@=ug_9F=6YGsnO~o{G^-yh>J(Zhyj<)aQ
zj%Rgx@+=CQs*R+I#xKss7sof=I=W5ScwKAE;)rD9<B2EN7l&s;AH+#~mdF_Bdte-S
zU(6y_@k)c{lWH#4PM&e~$R2{t>)m(dYYI#}5<{%>Pk4`I>=LUiS6Eu&O*GzmZe8!b
zbg#@RQ+9rr&`<YDWxU!fv(w1IiqKrdl&-UjU&`+U{Regjzs_0S3A!J--xbsDA1^0A
z9~^Ls-e?W$$NKi<kHzyJw_9^RfBh_)^}ytqGOugOKyvBB?E3~9Bvw~nO~%7aI^)?V
zN#0gTuM7{5uJcPb^E0KVpL<Yj?kI0(@ruKNl<H0?nZ!T|YyRei9qygq=o>lFSC=cp
zeyzHfC;SYVOuiYMXUI6_#d2hrtx1o<eCzqdCssUuubStW5@e`f>LrqFOO{e@BWu&|
zJ#qZe!(Ka?vdPLrOOGE5ji~!l@qM3YJ8c7fCagmipBp;zDl+fT1SZ?j?0m>i|B>;U
z+V}HH4J60+a2?&FD{9t}aP=%(m&D!i?u}k;1!Gon)&xn$lQbkb<{`&_?n<`n4QY(o
z@mweL=Dsae&!s<_RS2Cme8oXUW+d`xk*}2__p!@iR(k*DGm9tU+oRtc=M8-mBGtIQ
zhT|idq|MQH`I*t?ax-F<BF)$L@}Kgm+VHid|H6&uZ-eDtP`5rQ-FK4tN<pJlFP&ZF
zw|d$=d!$ER97;*${IqWUGnj?Wi%z?)ey$|>_{Li6f>m@F`@s)^>vFZ=Ku*}GE#3G;
z^@;Bm1=lMskKQDl9@%K4zy8PsH*HMx>6@C*yhdd%-4>94LBB2j#d>R%aHo#*Uzzr`
zw2)Z^1n}KSjd=W>l`HFWS2}%~C1X$HzN<3)f-h$HUM2iAzc1lju_Zb_r<7!WOxxm~
zKm&GWJB!<8T<ump@0jiHuze`==y*Y%W$zc9!yuViesE^tOg`h)b)gTQQZxA{w#gOT
z-$Ro7<L=LIdy=L+)ca%^8v6vD%f`4mZk37eeAvivB;`mne~I|C=Ld$4isz!#i=5g*
zq^@mk99!8Rj&xp_5g_rLJCar%aqMfV4$bR(#|iGt;-`0nc(LEHNZRlA{zwcT`vGM-
ztprooO{$ge*Qie)dgZ!MS`Zr7XFw{cugc~=cjM}ZOUU`$J0+)0-_`9EIVh;)H=p`+
zi@H<Ja!8ayfCH|bIIq-lS<`P{LuQ`bs1swA6+_+j=@GJlS}~T$jc<GhXi(LmSebFz
z&KMyBZ3d&+1-_=ITN6^Yh6*R~b$;FZb<e;LZ8p9=8GFlpz8YN4yAaf+(zH)rjP#mM
z>1nbY@|RaQKaP#O4~!Mieie3JUEx~rqZEO2mtVf#71}*ob1z?8yLal1ZRB^4s5I(3
zyHu-Bo?`cm-^E!i{X=M-Fzr;Ov(nxDbY*RuyO++r>^pUS*lm5tgSQlPw;rFHduU^E
z)L5ydrTp5OCa(uOrOFiN8mO0gZd@@x5iLY=_9k7*@WI`m&)x8mF#cf@kt@MppLTpq
zj5>)kJnGDkW;4F}spq|ea;Y~r_NCI21*N8)=C|6k)==PwK;%H{L(R<C*H?DldQ>ew
z9e+BP-ZsPHVuRA;mBTsvDoaxb-uU#Mjl5iR<<`#&&V#|Xz0atDsmhYkt8tn1JyI0X
z%tE4`X@%S|8&lNhrAc*s^q#Cy+PI^*xNfJ6PP=#VmG^bZ>|eySUTLs;pEgQ-VS8lh
z38lr9NKcGemD?v}BRS8w^HZl%%I<U&ncZ6AU!y#5JS?o$GtO;o&AO^*RwINXgw%DP
zqWLS=lT~uj`YmN|a_C$CeY8_ui=8TVpsMPOp<bEr!zVUe_bt9L=^diEJk~8kr#wKO
zBr7s<OwM4E#Zo)s#%P$s`S-=P?zMIDoju2|keyp9I*>Nw!}RFR=#G0LZYOX1-F4>h
zmx~L1di&5Tf0-8#1GkBdjlSedb=sr2N53GfWK@i_;6VNCiSUuHq|=4>`L(399m6+M
zO<v@4AE+MFcu`R_^B`<i)un;doHO<rY+2tJ@?)ZN_x{>*XU(?+ah8r8(VV>ZW%@eL
z-l%hqN{-?o6-p!HmRI(aJ-BqG?6!U5ogw4ncaM~N9X7Sx*ZVCknQ&TaS9*%k{daeC
z6g=ya1M}pCC>gp+tA0Mn$fPZFRTGt8cjQ#h_d_1nO5%zVGR#;LO+HlDj7%4=QKl0X
zoF`R$NG%&$e}Sh@mXYS8(7@vCaiQ?bp}~^{J*e}wkxJK{Nov~^AHhaP5sPi>7t{~F
zYJVx|(VvpV&oKQSl}ymT+CZ@w6lhbBynR=mu7%BpDZSeQO4;i#D*EX!5@riGl5cYg
zEK!xy;TT`Lev0*R`9OmW2f1c^heXivz3&oaUpq#0o}85O+FJeMA*)p|jp<O7!QLCT
zvHV}Hmqb4*&D}Pi*uX(!t(MPK5J`6RNk-WXrA0A$p2vIR@+r(cX=RuvGb<l$c}89P
zZAa&uMQI4sV%KyJ5favanojC2aG|;Dz`LL<6`KA{4hkeCtl#N`N!jC1u?LrK4=TO4
z;RtP7`#ae>3t_oeQS`cN9;+?AYJU7}bn#H9TgE}*YX<rix%qDlmP#**-!eaQNOtl^
zqnq`PUfsO6j2`p8Bt5m4jE$~dVC6g;GZ7H=&ihAPL~V}oGhTs{{ZzqM6?a@3u&C^=
z3Q!)`x1I{6A(QFnmY8g<_}ct7J@iiN8@t2VZgxUrR3)Du?3$%GO1Gufhq>u_mgvv0
zi{$nyFG8HI&Gu$%zae+D$&Ge7!T8)d_42oS)7!__JQ=*rE0*H_BRt^j4()K?<Tn?^
zVz%ChnF}sU@xS2Wzt}u^exYAN^UZmaJ4fcs_lb#7gx&CCNn$v1uhF1?lVLAw#HS8x
z;quR(8Fvm(e=GZvNU?wLgS^gNDoUC&KjqftsvJw(q@S327FpK0Qj~uvICx{h+2y5V
z!L(~hRfSxnRly|B*%SLt$vI{FGb(UIxpb(mH*p%x-K3~T({X)onW;l6v#65}k7%|?
z80+0J^9yx^tEH3)dzDp64{>s?vrD<|U-?Lx^L|!_28Y5K3!R4jpV+shU93+U7$N8%
zq9oNjyX&yEi_oatK&f1D9R1oKlmuhV@mW?uasO^cD;bwU$@ljUmWb@t6%l#;MvyF!
z!k#ac!L6WVA^b$Qzo%QLtg?8~Ri2Pimi_lw$8%k8cP!fFUt>JHKj=%y+G>V%&lGlT
zDo-)Ir+c7XS)N7w^T7xX`SYP-!tPthKP&cMx8rg;tMq|T)M;ePlKe<2)Avj^;q#BM
z&(}pxygl<Wv!&Z(cr4^aBKf`-H~Kb3ZDZz4OIheIoe;6Vb$?r-3oW7e;LQ--H-pC(
zN(HSauin4hT=4MglT9g~6D+3)Dubi-)=Nzu<Rbo?%g&8{d>(n=Fozk>u}}&HvP-Y2
z2jZd$;>wNZ<5HSOWbYVkS17LTFw{QelXKp@RwyltkBwXNzNl-2T}kORU+2Nkw_epy
zC3<fXwl3W~JmTWEc@qR2|FE-l;6xF}5sxvQK3eU{yUr>E`2#%l*Qi+e&7H))`3&bD
zQjCn<u3h`nRCFku*3v%5G}<9*j>VNS_Z+K;!V$`_<haHA-)Z)rJJjf}RQ6_C)&6Bk
z`Th}K%EdDE*KdtdUz)Vs6mU8cIFkLs!Iy&O)w5)i>4l2TZtr$&M05uzq=puinyqZM
z><SnWdV7M(q`zEjHsY(S%mxa*9^pX}A2Z&2Yd-Ikr&?2QDfDCW4v$N**Aw+gO%Ddu
ziPdQr(S>V1&Q;bvmG!V|;pt0We_5u@{mFq@PG3Af`-K^CUpW6{th&W-EaGamL)!Uc
zoK5{<PQ0f#?EA6w-sMAccBuR5L(^33uEryz<|p@99RBu!y~o>~{?W|sTV`XM=anM`
z2tnqzPJHn!G5j9QHLLD^v1wMx#Kf9rBK1dus9cEkk&71ti+u<5Z+^RCRJA3nxJ++A
zm|IA&i5(49wb|O$y?<XMJ+}4Sp=tXrQ623?!;F!jwkYjey-5*9!aJh+4z{#&(lzkX
z?xxxI!uyK{_sbX+HUjf&CK-=5{eco;wyh77WM6t0uxoMd^$8s5;Wr>3e*f^ZyYVBf
z&`Xnw8FPNck>nYQYG=K_uy4!Xz_G46Vasz#q;_mum9zI*$%6JX6Xa2^!cB@Jiqw4A
zD<2t|l;7?XZE`nMzW!?RLVZVKZqBVmz33VDQ*Dky&lRprJMRr;_SP~nnV(gEFnRNI
zrtJ;sC68l}cw4(+y`$FliJM=sH@$u+iHAJnhK3I-h1SiJy91a4?(USjd2^IMsEp=h
z!)NY?U6lF<48&p)zZ}O?`QB=00}ZuU^Let@d2`;vOO}2q9K$ibmkD>pJy^_2Wealr
zv(6MmZZ&ID*wxeCZ`K*TREgH-fBATNlbN?;K4a>G&bRLcgf3n=`05CSNvD(OzO`{l
zBA*4`snI*x7b&H0pG|B{2zS%T=a*YMcc!jVdTZ8DZ)Eyw^#!en@tf4)E#&;h$1lkL
z4AJso+_&G?WnkUt(`p9qK1&v{iugm$B(9R{_shiH;Av!&++*6MGGDkOOj3)oIQ)t8
zv6?ZJ0k-a83ga}vWR;0y_phn%V10PY`se|z{yKSSlZG;`lO@R}d1^1@JKyd2N_I8c
zgZn#8H=8M8GU<~I{mHE~0_7#0CpZ#Ec1CHdv-8*I_7AcZ1(4<}Ek&KHzi`AUBt~g4
zkO_TJ)*)Rpn<i>KuG4E(yGeI6e0<ZZPJ`q(u5X=Ax(?GmB;@QKef+Z{SG5Tv@P@c0
z_a^UHt8%W}s>fWs1K-4*ID7u)`ErIGWzRlvKOS_stJ}|RQMrTe_PZIr;}M77Ulibf
zz&2>IJvA=MJV>|bv77CI!S~08u9kG~>e!!TXLx%9nQK)-^oe;%HLG{W)@M-~%O5iU
za^?r`73%6rEcI)-<c^K9D|<CuS6OF%wu7Vavhex{`zS7^<nyUlRq}H3QpytFsrZEa
zs3$jy3W|Mpf%hQW==9mjM<#3C%H=NPUj1J2M%$FT(=s}*^knR<3*5Qawj~HBTLd*o
zYdjC>e*7WBS4#nXUfSg6AQ$^0xW>G>`jvC0A%EyP?R*34vNH+87BtpMyPV&uvd2~3
zRy2yTlDN=$IpEw(%enPymDs{|ap=WTA8U4slW!_WRKD@{R;Oj%xm2}%HRI<Q%Dm0|
z^F#Cf2QO2O#_idBSl;@lR>0Qd2J3HZ_*@}9-ew$8k{4gz64P$d=jwFQ*lc5n?{nK`
zN=hg0x!Bj>aj;WkwDp8*y_J3BCaOW-orQa?z39tTcP%#0cP`1F-d0tXPC6kjS}9j(
zkXwGJ(<{Qgqs5}3<4{J#cbDg0C`6;8HRKF+-fjQeo$GF1A8D{Iq_lJN+3xdLdEV9}
z(&f@xJ$gQkHk!v>ey*-1lFT<EFU=M+{LF26WSHO1t5Ld%sh`{Va^>Fk(9>)6T^mlt
zULSIxKV@3~G&Yu@?q%m76*ZfJnLwA#rzc&Xo0_H)@65G?q;r!V{qmE!F#I?V$1z%=
zxxUWO7ZbbacMGZJo}5rJ<DP4}a*;&dYx^DcJlTd4gYBQE(vL?~Q1%=OH{o1+F%j)^
zeehXr6N3pAlZ)B`VcLX4d*N}9)r3T`bAa@d#PC}lo%-4-akWZ1=bXFjUsEugU2`Dw
zHA9NKf&GxkgqB@f%{HBAG4X(7ncKf*9EhdxiTru{u7Htin+ZANn0aAymTR~t)nL0s
z)t6V+2?N!euY`#?32Cr|+fRMAUECupMY`pEC+SvM&(Wf+doxp%_98wehRl)kS&eZF
zUjnmbE?o#q9<wnFH>9<eRralZ%-K$mnV5)|X<}@ut4cA}VxF43xHmn{;={=H<WuzI
z2e$@qI#T|aM6jox!|-Ro(#2OIny!xh!3%_H3OV~zDc>KR$e-Qvn)>478<z*`6rz&u
zUVHb@@mNn|V=0Fy>0TkGhpM0SZkvudH|2dh^k^^5WcjWt!QsKX>x7#bj>?{EVOVp(
zd&)iLnamf~I?n{Q&Y^)f{fF;O3s1hL)+c?DIFQfR#N4D*w@Z|7Qh&HIpy%%Wj@`qa
zpJ;2Y9+xqE9CMG!kE+pTZp3$6RmiQ8v%0~X`}eQcy|noHC$-f{s<O%JiDYfDrR;_V
zrJON|ccs@4=DbYx6MwA}R@+_XWLfa0t?4C~d-xdbgv73$ey1CcR^I!V66Hthq3lO4
z7tzfcdp@TBU5r$S3+J^mxm%pf$A9qbKXS;`zK%g%vWN96`xjqoFSwjABech(@FKr1
zsYdpKtbZDbU7UePy4{hRM;7-T?NFqvU1(0;bLPV>g@EGpPyPLyNcP(XRn^}=QC=Xu
zL&H&=^(Ce9w(uiw&+R7PaoLk@<7>uO*&%a@@3MQurzYg=<UL0BZ}rUVcxQ1|s5vLb
zoyM2d=+hFj964F)D>~JK)z8Ol>Kt~rOEadfyY25eR3tpEm7=!Qi0oE8-S#4v>7v6i
z5vKG?=LSAyUn8LX`A4hea*hi+ty3kRVH!~8eYHDz>^$>%0h3-0>6{*GArto#QJ2=}
zKe0aN*qcw=b1pGv$!%@O)+l-7M+xo+nSE$ZT*+b>ImR+;JQbPhA{+L~>*rawRJs}M
zKwe2XfuFXwbw6lrJj>Ir(kgEnb49jwrm$me<(&=v&qKD;S*9~(|74i6Sif)MB;&{G
zUTOWDgE=MP9eEyKEj}H7kXQivx2W7=?b%Wq4gK31zn_?7N9QL^Y7dTmJ2fZP!{N6t
ztF+QufO0O!VcdPUb>86@3bl)yx!r9&7hT3XpKH+WwGE*pk=59I<jSeWAD^#IC|3wD
zTAN&petU7$N$f`1v&_@%)|Vp+)=ZI2H@3;lU%c>%g>5XX_&VE$^RH&Fb89Hl?+Zk|
zZ~J#zZ<a_Cl+)8p<Y+#ac)a1^%lms=ev&T~*;8conjLq&b=)BKR9w(qy-#wSlj7vI
zeg3yg?&$m9s@E;aN{w8gqb#NRp_l*OAVB9+)@iRpyx~_SW2h~@lr-<<RMU<<oF=#b
zZgK6E6U>V-z9;Poo+Za_$*I@-zmzvL9;<z=Eg?GPmGoBs%ZG_6!_efrcj?4;ip-LJ
zyO2=5CPu1QgL%6-`=M+q%J!J!DVHXdr&Ij6uI~3ezlT>V<rLZXm-*BW`Ob#rRk3c<
z`_K(a3Eo1wC+jZT39?zAx+wkBY^pcKsp*1<2J`EJUD9VF&!rtJY&X8qVVHdEVt5N1
zspoNVE&sieHU*_LgdML90y>fqqvq}}Z1?mNDpbcJDxMzs$opeNgiD?`_i8&uaAwc5
z2+M?hG2ZEx&uBko5A7dea&~(mH}Z*&LQdAi+hs!kYbtxoYs0s$fh70Wx6>-UTl<i+
z$9G;)L+R3iy!TuA_8z;MeP|Pf@)~*m8~!(b1bj61G$oy~d%bm^N%nEdY^D3{PHgAS
zTtDzWOFMN|u&tQ;r+(`Xx&tqoqW3hNJmYeCI?Z2i|69c~Y|PnD?kkKA@xSPo)4T9i
z)Qax8^@%SB7582p*`vk0zR2&|R(JgqF?l<69VYHRF+F2C8L@P8??+g_L7OuAPRdO#
z<-S9Qj_+985p?q8^{AucqqFP2wFYu?2H)G2@S2x<1AS4=*aget^fLpF(Y1K#&Uj60
zlUZqJqjh_K@!oq6yuRM2dl8kot=f`2mq&%SzHy76`(DnD%V+Yxg}>se^x*AlI{Y);
z|IsZdHHo|Y?TIp(9b=sIUI8`nmJ3G|uhL6U-QB-i{=j0z9qWm}y?Yu;+3yQC1foES
zpOn_qL0dj%y-8j?@7Maw_MP?2If+jU)!#$dFH}V3ejwS~T=s1rfhW<K!dy-1bxl-~
zt8A$tt)<yuA~#3%S-uAni?2vzJ!Gv0#XlV{G&y$ntZahbLt2a6G>vt)m9^J(Ri5<r
z(0F*GpqX>%b@r`|Zi2CCrW*pXpT6lUQFGVZBWSP1;=cckh{lf7logfNodYM1H?5%&
z7hiB-^t--ebJjJonWOK7xjlX07`=#`j7`kx&&ehqq6*!2Yza0_%iNsUsA%dkc`41F
zZe*mijWhYE(V<|Ch9VESaYg1qkIk;^nS~4c4ww#CJhVLF#irZYv-6PZ=>rr3O~uUF
zJU_zreRlppdQRAb#o$C_Zr9;248ANzkq)%xu`<bFS$8_-zVN@lm%pU2Snyg?&9(fc
z*+vT5qUxP~6&-`~DRFl~%+zP(FBZKnN^X4Azs>Z~y|l8xFi+a+R8yl@Y8^J|U#E8E
zE%TOp^{TUkG@1PI_$zMD4}P)ts5MkcWm@%u@_{N>uU<T@a9@baUX!N6Ga1d%JVAHn
zaxY4}Q%k%sbLo!9>`6;q2BxG}AMaioFrlK@qO3!v&!(`3?Yqmp0mElHI|7hHWQ}u1
zOj_fS>ou_kb<bWui@tDHJ~U76t5dh(jq9!f+9sDb`B9~rt|R?KKXB)O>7|e}Jp}Xf
zYcDbNJYc>Tds^6xCe>j5`pxzme_nAh5YV+V`xqo!etI+cjj?Wx^#%`_x{hsbeCn!h
zF~z&iKrzd!_3XlZhM~6!-W$?RliXH%SR+C2^R_SG<ILgbcHXgl-y3q-8Ie`2z4+5R
zE^DklK5KPUjd6mX5t>r5-w(x{;;XkY^6v^@rL=5n`pTMR!He8(So)H5+u-Hp2^;yz
zk+SE${a+4D<O%~F#q>+IA4*3ovwMf@*iv1>cw<Gn;weKtHg!Z)zsnxKCC#z^@tTir
zRjFGfPxjxVI6Nr<?v?C3H5AEZi=tW|dX;#C+S{Dpvw9f>)NPdQ@+l4K*y~$2&U8$B
zP9;jphc~-KgU`pq_Wd-Ap;Z^R+$n{_ZC~H%f0kSmKI2&ppQ%~H&Yl){AGgN%MM<ud
z^N#cziiaj>Jr=j^-nwyINKE{|h(}!xH`!5&!?js2S$UjK+~nLEo+GGvfVDXCu+yur
ziz(BF?l;s;p)c%7T&dAjlkwD#^BWvz7q9BNbz9#(#CtU?c1G~Rpwj*1Yo`tj<xX(d
zR7-I_wz(eq{Fx$U#&rB0@9LJH6hWl9-kBEjI_HKpqGVaD{VWQLh5XjtAAWxMGE(TW
zb^f@1BhMPPbCL3+l$U5ve`6@miZ>)|DDI99*E!g<P!;+qJ)f`Y*jd@ZP2T6a*mmaG
z)7-t@b6(8Ek1}gGPte0RNH@u0NPcVNZF35-_e#dE_OX86Z*D-hlij6A;{K6smyD?%
zhTm%PpnO6yFEqQ&{pdY;aXM{j`NuEg=o=p`(K}E*x_Ou*>4*M)gQ?EE`LuBF5DVGv
zSGq5EP|<Q4nv*rgIrRRBp5zkj+r5x)!o9=#2bo7&!J~M6vZtO)G3Cajd`>sW4qC@Z
zoto)M$a%`SG|YQ#O5qVd7YX;6r^UA6(GPi(i!`OJNfY6A-``Lh^88p!Lhd6ft$pa&
zcRL=*D+lvGFP<DVqPlp^G>EK8zTNKf5vftdCnc4AW7WJ%wIpZW22v2lzq1~1^|+*6
zVLSL}Yn^Gwde!E)huT8KKiY9_6p>~mB;+~S-pJ$1WfEqzrqtOjcUtmW2g^)Nu42ww
z1&cR%vKBNiqj<0Mv`mC2^VIq6C%wM+xPxiY#gaX&yxGhZ)>5Nz;l-@J&xV4+#ItYX
zT~0n1e75^8-Fl%bU{X|kG&=s&RioDPf_v9LR-oM~^mbR|t<V1J1|Av#i5^-M7J5l$
Icxmnb0q@zo=Kufz
new file mode 100644
index 0000000000000000000000000000000000000000..cb046a0e7ff67feeea570b8c395330d5fce4a2fc
GIT binary patch
literal 2882
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
new file mode 100644
index 0000000000000000000000000000000000000000..7c3f61a5e97d6a83c895e640f88fb07bc5474a23
GIT binary patch
literal 8707
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
Vh6*yu|4In|Iq*NNrTt&@{{iu!6LJ6m
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinstall/tests/manifest.ini
@@ -0,0 +1,1 @@
+[test.py]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinstall/tests/test.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import mozinfo
+import mozinstall
+import mozfile
+import os
+import tarfile
+import tempfile
+import unittest
+import zipfile
+
+# Store file location at load time
+here = os.path.dirname(os.path.abspath(__file__))
+
+class TestMozInstall(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        """ Setting up stub installers """
+        cls.dmg = os.path.join(here, 'Installer-Stubs', 'firefox.dmg')
+        cls.exe = os.path.join(here, 'Installer-Stubs', 'firefox.exe')
+        cls.zipfile = os.path.join(here, 'Installer-Stubs', 'firefox.zip')
+        cls.bz2 = os.path.join(here, 'Installer-Stubs', 'firefox.tar.bz2')
+
+    def setUp(self):
+        self.tempdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        mozfile.rmtree(self.tempdir)
+
+    def test_get_binary(self):
+        """ Test mozinstall's get_binary method """
+
+        if mozinfo.isLinux:
+            installdir = mozinstall.install(self.bz2, self.tempdir)
+            binary = os.path.join(installdir, 'firefox')
+            self.assertEqual(binary, mozinstall.get_binary(installdir, 'firefox'))
+
+        elif mozinfo.isWin:
+            installdir_exe = mozinstall.install(self.exe,
+                                                os.path.join(self.tempdir, 'exe'))
+            binary_exe = os.path.join(installdir_exe, 'firefox', 'firefox',
+                                      'firefox.exe')
+            self.assertEqual(binary_exe, mozinstall.get_binary(installdir_exe,
+                             'firefox'))
+
+            installdir_zip = mozinstall.install(self.zipfile,
+                                                os.path.join(self.tempdir, 'zip'))
+            binary_zip = os.path.join(installdir_zip, 'firefox.exe')
+            self.assertEqual(binary_zip, mozinstall.get_binary(installdir_zip,
+                             'firefox'))
+
+        elif mozinfo.isMac:
+            installdir = mozinstall.install(self.dmg, self.tempdir)
+            binary = os.path.join(installdir, 'Contents', 'MacOS', 'firefox')
+            self.assertEqual(binary, mozinstall.get_binary(installdir, 'firefox'))
+
+    def test_get_binary_error(self):
+        """ Test an InvalidBinary error is raised """
+
+        tempdir_empty = tempfile.mkdtemp()
+        self.assertRaises(mozinstall.InvalidBinary, mozinstall.get_binary,
+                          tempdir_empty, 'firefox')
+        mozfile.rmtree(tempdir_empty)
+
+    def test_is_installer(self):
+        """ Test we can identify a correct installer """
+
+        if mozinfo.isLinux:
+            self.assertTrue(mozinstall.is_installer(self.bz2))
+
+        if mozinfo.isWin:
+            # test zip installer
+            self.assertTrue(mozinstall.is_installer(self.zipfile))
+            # test exe installer
+            self.assertTrue(mozinstall.is_installer(self.exe))
+
+        if mozinfo.isMac:
+            self.assertTrue(mozinstall.is_installer(self.dmg))
+
+    def test_invalid_source_error(self):
+        """ Test InvalidSource error is raised with an incorrect installer """
+
+        if mozinfo.isLinux:
+            self.assertRaises(mozinstall.InvalidSource, mozinstall.install,
+                              self.dmg, 'firefox')
+
+        elif mozinfo.isWin:
+            self.assertRaises(mozinstall.InvalidSource, mozinstall.install,
+                              self.bz2, 'firefox')
+
+        elif mozinfo.isMac:
+            self.assertRaises(mozinstall.InvalidSource, mozinstall.install,
+                              self.exe, 'firefox')
+
+    def test_install(self):
+        """ Test mozinstall's install capability """
+
+        if mozinfo.isLinux:
+            installdir = mozinstall.install(self.bz2, self.tempdir)
+            self.assertEqual(os.path.join(self.tempdir, 'firefox'), installdir)
+
+        elif mozinfo.isWin:
+            installdir_exe = mozinstall.install(self.exe,
+                                                os.path.join(self.tempdir, 'exe'))
+            self.assertEqual(os.path.join(self.tempdir, 'exe', 'firefox'),
+                             installdir_exe)
+
+            installdir_zip = mozinstall.install(self.zipfile,
+                                                os.path.join(self.tempdir, 'zip'))
+            self.assertEqual(os.path.join(self.tempdir, 'zip', 'firefox'),
+                             installdir_zip)
+
+        elif mozinfo.isMac:
+            installdir = mozinstall.install(self.dmg, self.tempdir)
+            self.assertEqual(os.path.join(os.path.realpath(self.tempdir),
+                                          'FirefoxStub.app'), installdir)
+
+    def test_uninstall(self):
+        """ Test mozinstall's uninstall capabilites """
+        # Uninstall after installing
+
+        if mozinfo.isLinux:
+            installdir = mozinstall.install(self.bz2, self.tempdir)
+            mozinstall.uninstall(installdir)
+            self.assertFalse(os.path.exists(installdir))
+
+        elif mozinfo.isWin:
+            # Exe installer for Windows
+            installdir_exe = mozinstall.install(self.exe,
+                                                os.path.join(self.tempdir, 'exe'))
+            mozinstall.uninstall(installdir_exe)
+            self.assertFalse(os.path.exists(installdir_exe))
+
+            # Zip installer for Windows
+            installdir_zip = mozinstall.install(self.zipfile,
+                                                os.path.join(self.tempdir, 'zip'))
+            mozinstall.uninstall(installdir_zip)
+            self.assertFalse(os.path.exists(installdir_zip))
+
+        elif mozinfo.isMac:
+            installdir = mozinstall.install(self.dmg, self.tempdir)
+            mozinstall.uninstall(installdir)
+            self.assertFalse(os.path.exists(installdir))
+
+if __name__ == '__main__':
+    unittest.main()
deleted file mode 100644
--- a/testing/mozbase/mozlog/README.md
+++ /dev/null
@@ -1,18 +0,0 @@
-[Mozlog](https://github.com/mozilla/mozbase/tree/master/mozlog)
-is a python package intended to simplify and standardize logs in the Mozilla universe.
-It wraps around python's [logging](http://docs.python.org/library/logging.html)
-module and adds some additional functionality.
-
-# Usage
-
-Import mozlog instead of [logging](http://docs.python.org/library/logging.html)
-(all functionality in the logging module is also available from the mozlog module).
-To get a logger, call mozlog.getLogger passing in a name and the path to a log file.
-If no log file is specified, the logger will log to stdout.
-
-    import mozlog
-    logger = mozlog.getLogger('LOG_NAME', 'log_file_path')
-    logger.setLevel(mozlog.DEBUG)
-    logger.info('foo')
-    logger.testPass('bar')
-    mozlog.shutdown()
--- a/testing/mozbase/mozlog/mozlog/__init__.py
+++ b/testing/mozbase/mozlog/mozlog/__init__.py
@@ -1,5 +1,10 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
+"""Mozlog aims to standardize log formatting within Mozilla.
+It simply wraps Python's logging_ module and adds a few convenience methods for logging test results and events.
+"""
+
 from logger import *
+from loglistener import LogMessageServer
--- a/testing/mozbase/mozlog/mozlog/logger.py
+++ b/testing/mozbase/mozlog/mozlog/logger.py
@@ -2,55 +2,108 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from logging import getLogger as getSysLogger
 from logging import *
 # Some of the build slave environments don't see the following when doing
 # 'from logging import *'
 # see https://bugzilla.mozilla.org/show_bug.cgi?id=700415#c35
-from logging import getLoggerClass, addLevelName, setLoggerClass, shutdown
+from logging import getLoggerClass, addLevelName, setLoggerClass, shutdown, debug, info, basicConfig
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
 _default_level = INFO
 _LoggerClass = getLoggerClass()
 
 # Define mozlog specific log levels
 START      = _default_level + 1
 END        = _default_level + 2
 PASS       = _default_level + 3
 KNOWN_FAIL = _default_level + 4
 FAIL       = _default_level + 5
+CRASH      = _default_level + 6
 # Define associated text of log levels
 addLevelName(START, 'TEST-START')
 addLevelName(END, 'TEST-END')
 addLevelName(PASS, 'TEST-PASS')
 addLevelName(KNOWN_FAIL, 'TEST-KNOWN-FAIL')
 addLevelName(FAIL, 'TEST-UNEXPECTED-FAIL')
+addLevelName(CRASH, 'PROCESS-CRASH')
 
-class _MozLogger(_LoggerClass):
+class MozLogger(_LoggerClass):
     """
-    MozLogger class which adds three convenience log levels
-    related to automated testing in Mozilla
+    MozLogger class which adds some convenience log levels
+    related to automated testing in Mozilla and ability to
+    output structured log messages.
     """
     def testStart(self, message, *args, **kwargs):
+        """Logs a test start message"""
         self.log(START, message, *args, **kwargs)
 
     def testEnd(self, message, *args, **kwargs):
+        """Logs a test end message"""
         self.log(END, message, *args, **kwargs)
 
     def testPass(self, message, *args, **kwargs):
+        """Logs a test pass message"""
         self.log(PASS, message, *args, **kwargs)
 
     def testFail(self, message, *args, **kwargs):
+        """Logs a test fail message"""
         self.log(FAIL, message, *args, **kwargs)
 
     def testKnownFail(self, message, *args, **kwargs):
+        """Logs a test known fail message"""
         self.log(KNOWN_FAIL, message, *args, **kwargs)
 
-class _MozFormatter(Formatter):
+    def processCrash(self, message, *args, **kwargs):
+        """Logs a process crash message"""
+        self.log(CRASH, message, *args, **kwargs)
+
+    def log_structured(self, action, params=None):
+        """Logs a structured message object."""
+        if (params is None):
+            params = {}
+
+        level = params.get('_level', _default_level)
+        if isinstance(level, int):
+            params['_level'] = getLevelName(level)
+        else:
+            params['_level'] = level
+            level = getLevelName(level.upper())
+
+            # If the logger is fed a level number unknown to the logging
+            # module, getLevelName will return a string. Unfortunately,
+            # the logging module will raise a type error elsewhere if
+            # the level is not an integer.
+            if not isinstance(level, int):
+                level = _default_level
+
+        params['_namespace'] = self.name
+        params['action'] = action
+
+        message = params.get('message', 'UNKNOWN')
+        self.log(level, message, extra={'params': params})
+
+class JSONFormatter(Formatter):
+    """Log formatter for emitting structured JSON entries."""
+
+    def format(self, record):
+        params = getattr(record, 'params')
+        params['_time'] = int(round(record.created * 1000, 0))
+
+        if params.get('indent') is not None:
+            return json.dumps(params, indent=params['indent'])
+
+        return json.dumps(params)
+
+class MozFormatter(Formatter):
     """
     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')
 
@@ -69,34 +122,41 @@ class _MozFormatter(Formatter):
             if len(record.levelname) <= self.max_level_length:
                 self.level_length = len(record.levelname)
         else:
             pad = self.level_length - len(record.levelname) + 1
         sep = '|'.rjust(pad)
         fmt = '%(name)s %(levelname)s ' + sep + ' %(message)s'
         return fmt % record.__dict__
 
-def getLogger(name, logfile=None):
+def getLogger(name, handler=None):
     """
     Returns the logger with the specified name.
     If the logger doesn't exist, it is created.
+    If handler is specified, adds it to the logger. Otherwise a default handler
+    that logs to standard output will be used.
 
-    name       - The name of the logger to retrieve
-    [filePath] - If specified, the logger will log to the specified filePath
-                 Otherwise, the logger logs to stdout
-                 This parameter only has an effect if the logger doesn't exist
+    :param name: The name of the logger to retrieve
+    :param handler: A handler to add to the logger. If the logger already exists,
+                    and a handler is specified, an exception will be raised. To
+                    add a handler to an existing logger, call that logger's
+                    addHandler method.
     """
-    setLoggerClass(_MozLogger)
+    setLoggerClass(MozLogger)
 
     if name in Logger.manager.loggerDict:
-        return getSysLogger(name)
+        if (handler):
+            raise ValueError('The handler parameter requires ' + \
+                             'that a logger by this name does ' + \
+                             'not already exist')
+        return Logger.manager.loggerDict[name]
 
     logger = getSysLogger(name)
     logger.setLevel(_default_level)
 
-    if logfile:
-        handler = FileHandler(logfile)
-    else:
+    if handler is None:
         handler = StreamHandler()
-    handler.setFormatter(_MozFormatter())
+        handler.setFormatter(MozFormatter())
+
     logger.addHandler(handler)
+    logger.propagate = False
     return logger
 
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozlog/mozlog/loglistener.py
@@ -0,0 +1,50 @@
+# 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 SocketServer
+import socket
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+class LogMessageServer(SocketServer.TCPServer):
+    def __init__(self, server_address, logger, message_callback=None, timeout=3):
+        SocketServer.TCPServer.__init__(self, server_address, LogMessageHandler)
+        self._logger = logger
+        self._message_callback = message_callback
+        self.timeout = timeout
+
+class LogMessageHandler(SocketServer.BaseRequestHandler):
+    """Processes output from a connected log source, logging to an
+    existing logger upon receipt of a well-formed log messsage."""
+
+    def handle(self):
+        """Continually listens for log messages."""
+        self._partial_message = ''
+        self.request.settimeout(self.server.timeout)
+
+        while True:
+            try:
+                data = self.request.recv(1024)
+                if not data:
+                    return
+                self.process_message(data)
+            except socket.timeout:
+                return
+
+    def process_message(self, data):
+        """Processes data from a connected log source. Messages are assumed
+        to be newline delimited, and generally well-formed JSON."""
+        for part in data.split('\n'):
+            msg_string = self._partial_message + part
+            try:
+                msg = json.loads(msg_string)
+                self._partial_message = ''
+                self.server._logger.log_structured(msg.get('action', 'UNKNOWN'), msg)
+                if self.server._message_callback:
+                    self.server._message_callback()
+
+            except ValueError:
+                self._partial_message = msg_string
--- a/testing/mozbase/mozlog/setup.py
+++ b/testing/mozbase/mozlog/setup.py
@@ -1,38 +1,29 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
-import os
-import sys
 from setuptools import setup
 
 PACKAGE_NAME = "mozlog"
-PACKAGE_VERSION = "1.1"
-
-desc = """Robust log handling specialized for logging in the Mozilla universe"""
-# take description from README
-here = os.path.dirname(os.path.abspath(__file__))
-try:
-    description = file(os.path.join(here, 'README.md')).read()
-except IOError, OSError:
-    description = ''
+PACKAGE_VERSION = '1.3'
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
-      description=desc,
-      long_description=description,
+      description="Robust log handling specialized for logging in the Mozilla universe",
+      long_description="see http://mozbase.readthedocs.org/",
       author='Mozilla Automation and Testing Team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
-      license='MPL 2',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
+      license='MPL 1.1/GPL 2.0/LGPL 2.1',
       packages=['mozlog'],
       zip_safe=False,
+      tests_require=['mozfile'],
       platforms =['Any'],
       classifiers=['Development Status :: 4 - Beta',
                    'Environment :: Console',
                    'Intended Audience :: Developers',
-                   'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
-                   ' Operating System :: OS Independent',
+                   'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
+                   'Operating System :: OS Independent',
                    'Topic :: Software Development :: Libraries :: Python Modules',
                   ]
      )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozlog/tests/manifest.ini
@@ -0,0 +1,1 @@
+[test_logger.py]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozlog/tests/test_logger.py
@@ -0,0 +1,170 @@
+# 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 mozlog
+import mozfile
+import unittest
+import socket
+import time
+import threading
+import json
+
+class ListHandler(mozlog.Handler):
+    """Mock handler appends messages to a list for later inspection."""
+
+    def __init__(self):
+        mozlog.Handler.__init__(self)
+        self.messages = []
+
+    def emit(self, record):
+        self.messages.append(self.format(record))
+
+class TestLogging(unittest.TestCase):
+    """Tests behavior of basic mozlog api."""
+
+    def test_logger_defaults(self):
+        """Tests the default logging format and behavior."""
+
+        default_logger = mozlog.getLogger('default.logger')
+        self.assertEqual(default_logger.name, 'default.logger')
+        self.assertEqual(len(default_logger.handlers), 1)
+        self.assertTrue(isinstance(default_logger.handlers[0],
+                                   mozlog.StreamHandler))
+
+        f = mozfile.NamedTemporaryFile()
+        list_logger = mozlog.getLogger('file.logger',
+                                       handler=mozlog.FileHandler(f.name))
+        self.assertEqual(len(list_logger.handlers), 1)
+        self.assertTrue(isinstance(list_logger.handlers[0],
+                                   mozlog.FileHandler))
+        f.close()
+
+        self.assertRaises(ValueError, mozlog.getLogger,
+                          'file.logger', handler=ListHandler())
+
+class TestStructuredLogging(unittest.TestCase):
+    """Tests structured output in mozlog."""
+
+    def setUp(self):
+        self.handler = ListHandler()
+        self.handler.setFormatter(mozlog.JSONFormatter())
+        self.logger = mozlog.MozLogger('test.Logger')
+        self.logger.addHandler(self.handler)
+        self.logger.setLevel(mozlog.DEBUG)
+
+    def check_messages(self, expected, actual):
+        """Checks actual for equality with corresponding fields in actual.
+        The actual message should contain all fields in expected, and
+        should be identical, with the exception of the timestamp field.
+        The actual message should contain no fields other than the timestamp
+        field and those present in expected."""
+
+        self.assertTrue(isinstance(actual['_time'], (int, long)))
+
+        for k, v in expected.items():
+            self.assertEqual(v, actual[k])
+
+        for k in actual.keys():
+            if k != '_time':
+                self.assertTrue(expected.get(k) is not None)
+
+    def test_structured_output(self):
+        self.logger.log_structured('test_message',
+                                   {'_level': mozlog.INFO,
+                                    'message': 'message one'})
+        self.logger.log_structured('test_message',
+                                   {'_level': mozlog.INFO,
+                                    'message': 'message two'})
+
+        message_one_expected = {'_namespace': 'test.Logger',
+                                '_level': 'INFO',
+                                'message': 'message one',
+                                'action': 'test_message'}
+        message_two_expected = {'_namespace': 'test.Logger',
+                                '_level': 'INFO',
+                                'message': 'message two',
+                                'action': 'test_message'}
+
+        message_one_actual = json.loads(self.handler.messages[0])
+        message_two_actual = json.loads(self.handler.messages[1])
+
+        self.check_messages(message_one_expected, message_one_actual)
+        self.check_messages(message_two_expected, message_two_actual)
+
+    def message_callback(self):
+        if len(self.handler.messages) == 3:
+            message_one_expected = {'_namespace': 'test.Logger',
+                                    '_level': 'DEBUG',
+                                    'message': 'socket message one',
+                                    'action': 'test_message'}
+            message_two_expected = {'_namespace': 'test.Logger',
+                                    '_level': 'DEBUG',
+                                    'message': 'socket message two',
+                                    'action': 'test_message'}
+            message_three_expected = {'_namespace': 'test.Logger',
+                                      '_level': 'DEBUG',
+                                      'message': 'socket message three',
+                                      'action': 'test_message'}
+
+            message_one_actual = json.loads(self.handler.messages[0])
+
+            message_two_actual = json.loads(self.handler.messages[1])
+
+            message_three_actual = json.loads(self.handler.messages[2])
+
+            self.check_messages(message_one_expected, message_one_actual)
+            self.check_messages(message_two_expected, message_two_actual)
+            self.check_messages(message_three_expected, message_three_actual)
+
+    def test_log_listener(self):
+        connection = '127.0.0.1', 0
+        self.log_server = mozlog.LogMessageServer(connection,
+                                                  self.logger,
+                                                  message_callback=self.message_callback,
+                                                  timeout=0.5)
+
+        # The namespace fields of these messages will be overwritten.
+        message_string_one = json.dumps({'message': 'socket message one',
+                                         'action': 'test_message',
+                                         '_level': 'DEBUG',
+                                         '_namespace': 'foo.logger'})
+
+        message_string_two = json.dumps({'message': 'socket message two',
+                                         'action': 'test_message',
+                                         '_level': 'DEBUG',
+                                         '_namespace': 'foo.logger'})
+
+        message_string_three = json.dumps({'message': 'socket message three',
+                                           'action': 'test_message',
+                                           '_level': 'DEBUG',
+                                           '_namespace': 'foo.logger'})
+
+        message_string = message_string_one + '\n' + \
+                         message_string_two + '\n' + \
+                         message_string_three + '\n'
+
+        server_thread = threading.Thread(target=self.log_server.handle_request)
+        server_thread.start()
+
+        host, port = self.log_server.server_address
+
+        sock = socket.socket()
+        sock.connect((host, port))
+
+        # Sleeps prevent listener from receiving entire message in a single call
+        # to recv in order to test reconstruction of partial messages.
+        sock.sendall(message_string[:8])
+        time.sleep(.01)
+        sock.sendall(message_string[8:32])
+        time.sleep(.01)
+        sock.sendall(message_string[32:64])
+        time.sleep(.01)
+        sock.sendall(message_string[64:128])
+        time.sleep(.01)
+        sock.sendall(message_string[128:])
+
+        server_thread.join()
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/moznetwork/moznetwork/__init__.py
+++ b/testing/mozbase/moznetwork/moznetwork/__init__.py
@@ -1,5 +1,24 @@
 # 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/.
 
+"""
+moznetwork is a very simple module designed for one task: getting the
+network address of the current machine.
+
+Example usage:
+
+::
+
+  import moznetwork
+
+  try:
+      ip = moznetwork.get_ip()
+      print "The external IP of your machine is '%s'" % ip
+  except moznetwork.NetworkError:
+      print "Unable to determine IP address of machine"
+      raise
+
+"""
+
 from moznetwork import *
--- a/testing/mozbase/moznetwork/moznetwork/moznetwork.py
+++ b/testing/mozbase/moznetwork/moznetwork/moznetwork.py
@@ -1,22 +1,22 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
-import os
 import socket
 import array
 import struct
-if os.name != 'nt':
+import mozinfo
+
+if mozinfo.isLinux:
     import fcntl
 
-
 class NetworkError(Exception):
-    """Unable to obtain interface or IP"""
+    """Exception thrown when unable to obtain interface or IP."""
 
 
 def _get_interface_list():
     """Provides a list of available network interfaces
        as a list of tuples (name, ip)"""
     max_iface = 32  # Maximum number of interfaces(Aribtrary)
     bytes = max_iface * 32
     is_32bit = (8 * struct.calcsize("P")) == 32  # Set Architecture
@@ -35,29 +35,31 @@ def _get_interface_list():
                 socket.inet_ntoa(namestr[i + 20:i + 24]))\
                 for i in range(0, outbytes, struct_size)]
 
     except IOError:
         raise NetworkError('Unable to call ioctl with SIOCGIFCONF')
 
 
 def get_ip():
-    """Provides an available network interface address. A
-       NetworkError exception is raised in case of failure."""
+    """Provides an available network interface address, for example
+       "192.168.1.3".
+
+       A `NetworkError` exception is raised in case of failure."""
     try:
         try:
             ip = socket.gethostbyname(socket.gethostname())
         except socket.gaierror:  # for Mac OS X
             ip = socket.gethostbyname(socket.gethostname() + ".local")
     except socket.gaierror:
         # sometimes the hostname doesn't resolve to an ip address, in which
         # case this will always fail
         ip = None
 
-    if (ip is None or ip.startswith("127.")) and os.name != "nt":
+    if (ip is None or ip.startswith("127.")) and mozinfo.isLinux:
         interfaces = _get_interface_list()
         for ifconfig in interfaces:
             if ifconfig[0] == 'lo':
                 continue
             else:
                 return ifconfig[1]
 
     if ip is None:
--- a/testing/mozbase/moznetwork/setup.py
+++ b/testing/mozbase/moznetwork/setup.py
@@ -1,25 +1,25 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from setuptools import setup
 
-PACKAGE_VERSION = '0.21'
+PACKAGE_VERSION = '0.22'
 
-deps=[]
+deps=[ 'mozinfo' ]
 
 setup(name='moznetwork',
       version=PACKAGE_VERSION,
       description="Library of network utilities for use in Mozilla testing",
       long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['moznetwork'],
       include_package_data=True,
       zip_safe=False,
       install_requires=deps
       )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/moznetwork/tests/manifest.ini
@@ -0,0 +1,3 @@
+[test.py]
+# Bug 892087 - Doesn't work reliably on osx
+skip-if = os == 'mac'
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/moznetwork/tests/test.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+"""
+Unit-Tests for moznetwork
+"""
+
+import mock
+import mozinfo
+import moznetwork
+import re
+import subprocess
+import unittest
+
+
+def verify_ip_in_list(ip):
+    """
+    Helper Method to check if `ip` is listed in Network Adresses
+    returned by ipconfig/ifconfig, depending on the platform in use
+
+    :param ip: IPv4 address in the xxx.xxx.xxx.xxx format as a string
+                Example Usage:
+                    verify_ip_in_list('192.168.0.1')
+
+    returns True if the `ip` is in the list of IPs in ipconfig/ifconfig
+    """
+
+    # Regex to match IPv4 addresses.
+    # 0-255.0-255.0-255.0-255, note order is important here.
+    regexip = re.compile("((25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}"
+                              "(25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)")
+
+    if mozinfo.isLinux or mozinfo.isMac or mozinfo.isBsd:
+        args = ["ifconfig"]
+
+    if mozinfo.isWin:
+        args = ["ipconfig"]
+
+    ps = subprocess.Popen(args, stdout=subprocess.PIPE)
+    standardoutput, standarderror = ps.communicate()
+
+    # Generate a list of IPs by parsing the output of ip/ifconfig
+    ip_list = [x.group() for x in re.finditer(regexip, standardoutput)]
+
+    # Check if ip is in list
+    if ip in ip_list:
+        return True
+    else:
+        return False
+
+
+class TestGetIP(unittest.TestCase):
+
+    def test_get_ip(self):
+        """ Attempt to test the IP address returned by
+        moznetwork.get_ip() is valid """
+
+        ip = moznetwork.get_ip()
+
+        # Check the IP returned by moznetwork is in the list
+        self.assertTrue(verify_ip_in_list(ip))
+
+    def test_get_ip_using_get_interface(self):
+        """ Test that the control flow path for get_ip() using
+        _get_interface_list() is works """
+
+        if mozinfo.isLinux:
+
+            with mock.patch('socket.gethostbyname') as byname:
+                # Force socket.gethostbyname to return None
+                byname.return_value = None
+
+                ip = moznetwork.get_ip()
+
+                # Check the IP returned by moznetwork is in the list
+                self.assertTrue(verify_ip_in_list(ip))
+
+
+if __name__ == '__main__':
+    unittest.main()
--- a/testing/mozbase/mozprocess/setup.py
+++ b/testing/mozbase/mozprocess/setup.py
@@ -16,17 +16,17 @@ setup(name='mozprocess',
                    'Natural Language :: English',
                    'Operating System :: OS Independent',
                    'Programming Language :: Python',
                    'Topic :: Software Development :: Libraries :: Python Modules',
                    ],
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL 2.0',
       packages=['mozprocess'],
       include_package_data=True,
       zip_safe=False,
       install_requires=['mozinfo'],
       entry_points="""
       # -*- Entry points: -*-
       """,
--- a/testing/mozbase/mozprocess/tests/Makefile
+++ b/testing/mozbase/mozprocess/tests/Makefile
@@ -21,17 +21,17 @@ iniparser:
 proclaunch.obj: proclaunch.c
 	@(echo "compiling proclaunch; platform: $(UNAME), WIN32: $(WIN32)")
 	$(CC) $(CFLAGS) proclaunch.c
 
 proclaunch: proclaunch.obj
 	$(LINK) $(LFLAGS) proclaunch.obj
 
 clean:
-	$(RM) proclaunch.exe projloaunch.obj
+	$(RM) proclaunch.exe proclaunch.obj
 else
 # *nix/Mac
 LFLAGS  = -L.. -liniparser
 AR	    = ar
 ARFLAGS = rcv
 RM      = rm -f
 CC      = gcc
 ifeq ($(UNAME), Linux)
--- a/testing/mozbase/mozprofile/mozprofile/__init__.py
+++ b/testing/mozbase/mozprofile/mozprofile/__init__.py
@@ -5,13 +5,14 @@
 """
 To use mozprofile as an API you can import mozprofile.profile_ and/or the AddonManager_.
 
 ``mozprofile.profile`` features a generic ``Profile`` class.  In addition,
 subclasses ``FirefoxProfile`` and ``ThundebirdProfile`` are available
 with preset preferences for those applications.
 """
 
-from profile import *
 from addons import *
 from cli import *
+from permissions import *
 from prefs import *
+from profile import *
 from webapps import *
--- a/testing/mozbase/mozprofile/mozprofile/permissions.py
+++ b/testing/mozbase/mozprofile/mozprofile/permissions.py
@@ -3,34 +3,34 @@
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 
 """
 add permissions to the profile
 """
 
 __all__ = ['MissingPrimaryLocationError', 'MultiplePrimaryLocationsError',
-           'DuplicateLocationError', 'BadPortLocationError',
+           'DEFAULT_PORTS', 'DuplicateLocationError', 'BadPortLocationError',
            'LocationsSyntaxError', 'Location', 'ServerLocations',
            'Permissions']
 
 import codecs
 import itertools
 import os
 try:
     import sqlite3
 except ImportError:
     from pysqlite2 import dbapi2 as sqlite3
 import urlparse
 
 # http://hg.mozilla.org/mozilla-central/file/b871dfb2186f/build/automation.py.in#l28
-_DEFAULT_PORTS = { 'http': '8888',
-                   'https': '4443',
-                   'ws': '4443',
-                   'wss': '4443' }
+DEFAULT_PORTS = { 'http': '8888',
+                  'https': '4443',
+                  'ws': '4443',
+                  'wss': '4443' }
 
 class LocationError(Exception):
     """Signifies an improperly formed location."""
 
     def __str__(self):
         s = "Bad location"
         if self.message:
             s += ": %s" % self.message
@@ -182,17 +182,17 @@ class ServerLocations(object):
             if '://' not in server:
                 server = 'http://' + server
             scheme, netloc, path, query, fragment = urlparse.urlsplit(server)
             # get the host and port
             try:
                 host, port = netloc.rsplit(':', 1)
             except ValueError:
                 host = netloc
-                port = _DEFAULT_PORTS.get(scheme, '80')
+                port = DEFAULT_PORTS.get(scheme, '80')
 
             try:
                 location = Location(scheme, host, port, options)
                 self.add(location, suppress_callback=True)
             except LocationError, e:
                 raise LocationsSyntaxError(lineno, e)
 
             new_locations.append(location)
@@ -292,17 +292,17 @@ class Permissions(object):
 
         return prefs, user_prefs
 
     def pac_prefs(self, user_proxy=None):
         """
         return preferences for Proxy Auto Config. originally taken from
         http://mxr.mozilla.org/mozilla-central/source/build/automation.py.in
         """
-        proxy = _DEFAULT_PORTS
+        proxy = DEFAULT_PORTS.copy()
 
         # We need to proxy every server but the primary one.
         origins = ["'%s'" % l.url()
                    for l in self._locations]
         origins = ", ".join(origins)
         proxy["origins"] = origins
 
         for l in self._locations:
--- a/testing/mozbase/mozprofile/mozprofile/prefs.py
+++ b/testing/mozbase/mozprofile/mozprofile/prefs.py
@@ -151,18 +151,26 @@ class Preferences(object):
             raise PreferencesReadError("Malformed preferences: %s" % path)
         types = (bool, basestring, int)
         if [i for i in values
             if not [isinstance(i, j) for j in types]]:
             raise PreferencesReadError("Only bool, string, and int values allowed")
         return prefs
 
     @classmethod
-    def read_prefs(cls, path, pref_setter='user_pref'):
-        """read preferences from (e.g.) prefs.js"""
+    def read_prefs(cls, path, pref_setter='user_pref', interpolation=None):
+        """
+        Read preferences from (e.g.) prefs.js
+
+        :param path: The path to the preference file to read.
+        :param pref_setter: The name of the function used to set preferences
+                            in the preference file.
+        :param interpolation: If provided, a dict that will be passed
+                              to str.format to interpolate preference values.
+        """
 
         comment = re.compile('/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/', re.MULTILINE)
 
         marker = '##//' # magical marker
         lines = [i.strip() for i in mozfile.load(path).readlines() if i.strip()]
         _lines = []
         for line in lines:
             if line.startswith(('#', '//')):
@@ -179,16 +187,18 @@ class Preferences(object):
         for token in tokenize.generate_tokens(f_obj.readline):
             if token[0] == tokenize.COMMENT:
                 continue
             processed_tokens.append(token[:2]) # [:2] gets around http://bugs.python.org/issue9974
         string = tokenize.untokenize(processed_tokens)
 
         retval = []
         def pref(a, b):
+            if interpolation and isinstance(b, basestring):
+                b = b.format(**interpolation)
             retval.append((a, b))
         lines = [i.strip().rstrip(';') for i in string.split('\n') if i.strip()]
 
         _globals = {'retval': retval, 'true': True, 'false': False}
         _globals[pref_setter] = pref
         for line in lines:
             try:
                 eval(line, _globals, {})
--- a/testing/mozbase/mozprofile/mozprofile/profile.py
+++ b/testing/mozbase/mozprofile/mozprofile/profile.py
@@ -1,13 +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/.
 
-__all__ = ['Profile', 'FirefoxProfile', 'ThunderbirdProfile']
+__all__ = ['Profile',
+           'FirefoxProfile',
+           'MetroFirefoxProfile',
+           'ThunderbirdProfile']
 
 import os
 import time
 import tempfile
 import types
 import uuid
 from addons import AddonManager
 from permissions import Permissions
@@ -249,21 +252,22 @@ class Profile(object):
                 self.addon_manager.clean_addons()
                 self.permissions.clean_db()
                 self.webapps.clean()
 
     __del__ = cleanup
 
 class FirefoxProfile(Profile):
     """Specialized Profile subclass for Firefox"""
+
     preferences = {# Don't automatically update the application
                    'app.update.enabled' : False,
                    # Don't restore the last open set of tabs if the browser has crashed
                    'browser.sessionstore.resume_from_crash': False,
-                   # Don't check for the default web browser
+                   # Don't check for the default web browser during startup
                    'browser.shell.checkDefaultBrowser' : False,
                    # Don't warn on exit when multiple tabs are open
                    'browser.tabs.warnOnClose' : False,
                    # Don't warn when exiting the browser
                    'browser.warnOnQuit': False,
                    # Don't send Firefox health reports to the production server
                    'datareporting.healthreport.documentServerURI' : 'http://%(server)s/healthreport/',
                    # Only install add-ons from the profile and the application scope
@@ -285,18 +289,57 @@ class FirefoxProfile(Profile):
                    'security.notification_enable_delay' : 0,
                    # Suppress automatic safe mode after crashes
                    'toolkit.startup.max_resumed_crashes' : -1,
                    # Don't report telemetry information
                    'toolkit.telemetry.enabled' : False,
                    'toolkit.telemetry.enabledPreRelease' : False,
                    }
 
+class MetroFirefoxProfile(Profile):
+    """Specialized Profile subclass for Firefox Metro"""
+
+    preferences = {# Don't automatically update the application for desktop and metro build
+                   'app.update.enabled' : False,
+                   'app.update.metro.enabled' : False,
+                   # Don't restore the last open set of tabs if the browser has crashed
+                   'browser.sessionstore.resume_from_crash': False,
+                   # Don't check for the default web browser during startup
+                   'browser.shell.checkDefaultBrowser' : False,
+                   # Don't send Firefox health reports to the production server
+                   'datareporting.healthreport.documentServerURI' : 'http://%(server)s/healthreport/',
+                   # Only install add-ons from the profile and the application scope
+                   # Also ensure that those are not getting disabled.
+                   # see: https://developer.mozilla.org/en/Installing_extensions
+                   'extensions.enabledScopes' : 5,
+                   'extensions.autoDisableScopes' : 10,
+                   # Don't install distribution add-ons from the app folder
+                   'extensions.installDistroAddons' : False,
+                   # Dont' run the add-on compatibility check during start-up
+                   'extensions.showMismatchUI' : False,
+                   # Disable strict compatibility checks to allow add-ons enabled by default
+                   'extensions.strictCompatibility' : False,
+                   # Don't automatically update add-ons
+                   'extensions.update.enabled'    : False,
+                   # Don't open a dialog to show available add-on updates
+                   'extensions.update.notifyUser' : False,
+                   # Enable test mode to run multiple tests in parallel
+                   'focusmanager.testmode' : True,
+                   # Suppress delay for main action in popup notifications
+                   'security.notification_enable_delay' : 0,
+                   # Suppress automatic safe mode after crashes
+                   'toolkit.startup.max_resumed_crashes' : -1,
+                   # Don't report telemetry information
+                   'toolkit.telemetry.enabled' : False,
+                   'toolkit.telemetry.enabledPreRelease' : False,
+                   }
+
 class ThunderbirdProfile(Profile):
     """Specialized Profile subclass for Thunderbird"""
+
     preferences = {'extensions.update.enabled'    : False,
                    'extensions.update.notifyUser' : False,
                    'browser.shell.checkDefaultBrowser' : False,
                    'browser.tabs.warnOnClose' : False,
                    'browser.warnOnQuit': False,
                    'browser.sessionstore.resume_from_crash': False,
                    # prevents the 'new e-mail address' wizard on new profile
                    'mail.provider.enabled': False,
--- a/testing/mozbase/mozprofile/setup.py
+++ b/testing/mozbase/mozprofile/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 sys
 from setuptools import setup
 
-PACKAGE_VERSION = '0.9'
+PACKAGE_VERSION = '0.12'
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
 deps = ["ManifestDestiny >= 0.5.4",
         "mozfile >= 0.6"]
 # version-dependent dependencies
 try:
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js
@@ -0,0 +1,4 @@
+user_pref("browser.foo", "http://{server}");
+user_pref("zoom.minPercent", 30);
+user_pref("webgl.verbose", "false");
+user_pref("browser.bar", "{abc}xyz");
--- a/testing/mozbase/mozprofile/tests/test_preferences.py
+++ b/testing/mozbase/mozprofile/tests/test_preferences.py
@@ -256,16 +256,34 @@ user_pref("webgl.force-enabled", true);
         self.assertEqual(read_prefs, _prefs)
 
     def test_read_prefs_with_comments(self):
         """test reading preferences from a prefs.js file that contains comments"""
 
         path = os.path.join(here, 'files', 'prefs_with_comments.js')
         self.assertEqual(dict(Preferences.read_prefs(path)), self._prefs_with_comments)
 
+    def test_read_prefs_with_interpolation(self):
+        """test reading preferences from a prefs.js file whose values
+        require interpolation"""
+
+        expected_prefs = {
+            "browser.foo": "http://server-name",
+            "zoom.minPercent": 30,
+            "webgl.verbose": "false",
+            "browser.bar": "somethingxyz"
+            }
+        values = {
+            "server": "server-name",
+            "abc": "something"
+            }
+        path = os.path.join(here, 'files', 'prefs_with_interpolation.js')
+        read_prefs = Preferences.read_prefs(path, interpolation=values)
+        self.assertEqual(dict(read_prefs), expected_prefs)
+
     def test_read_prefs_ttw(self):
         """test reading preferences through the web via mozhttpd"""
 
         # create a MozHttpd instance
         docroot = os.path.join(here, 'files')
         host = '127.0.0.1'
         port = 8888
         httpd = mozhttpd.MozHttpd(host=host, port=port, docroot=docroot)
--- a/testing/mozbase/mozrunner/mozrunner/__init__.py
+++ b/testing/mozbase/mozrunner/mozrunner/__init__.py
@@ -1,5 +1,9 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
+from local import *
+from local import LocalRunner as Runner
+from remote import *
 
-from runner import *
+runners = local_runners
+runners.update(remote_runners)
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/local.py
@@ -0,0 +1,386 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+__all__ = ['CLI',
+           'cli',
+           'package_metadata',
+           'LocalRunner',
+           'local_runners',
+           'FirefoxRunner',
+           'MetroFirefoxRunner',
+           'ThunderbirdRunner']
+
+import mozinfo
+import optparse
+import os
+import platform
+import subprocess
+import sys
+import ConfigParser
+
+from utils import get_metadata_from_egg
+from utils import findInPath
+from mozprofile import Profile, FirefoxProfile, MetroFirefoxProfile, ThunderbirdProfile, MozProfileCLI
+from runner import Runner
+
+if mozinfo.isMac:
+    from plistlib import readPlist
+
+package_metadata = get_metadata_from_egg('mozrunner')
+
+# Map of debugging programs to information about them
+# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59
+debuggers = {'gdb': {'interactive': True,
+                     'args': ['-q', '--args'],},
+             'valgrind': {'interactive': False,
+                          'args': ['--leak-check=full']}
+             }
+
+def debugger_arguments(debugger, arguments=None, interactive=None):
+    """
+    finds debugger arguments from debugger given and defaults
+    * debugger : debugger name or path to debugger
+    * arguments : arguments to the debugger, or None to use defaults
+    * interactive : whether the debugger should be run in interactive mode, or None to use default
+    """
+
+    # find debugger executable if not a file
+    executable = debugger
+    if not os.path.exists(executable):
+        executable = findInPath(debugger)
+    if executable is None:
+        raise Exception("Path to '%s' not found" % debugger)
+
+    # if debugger not in dictionary of knowns return defaults
+    dirname, debugger = os.path.split(debugger)
+    if debugger not in debuggers:
+        return ([executable] + (arguments or []), bool(interactive))
+
+    # otherwise use the dictionary values for arguments unless specified
+    if arguments is None:
+        arguments = debuggers[debugger].get('args', [])
+    if interactive is None:
+        interactive = debuggers[debugger].get('interactive', False)
+    return ([executable] + arguments, interactive)
+
+class LocalRunner(Runner):
+    """Handles all running operations. Finds bins, runs and kills the process."""
+
+    profile_class = Profile # profile class to use by default
+
+    @classmethod
+    def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None,
+               clean_profile=True, process_class=None):
+        profile = cls.profile_class(**(profile_args or {}))
+        return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs,
+                                           clean_profile=clean_profile, process_class=process_class)
+
+    def __init__(self, profile, binary, cmdargs=None, env=None,
+                 kp_kwargs=None, clean_profile=None, process_class=None):
+
+        super(LocalRunner, self).__init__(profile, clean_profile=clean_profile, kp_kwargs=None,
+                                               process_class=process_class, env=None)
+
+        # find the binary
+        self.binary = binary
+        if not self.binary:
+            raise Exception("Binary not specified")
+        if not os.path.exists(self.binary):
+            raise OSError("Binary path does not exist: %s" % self.binary)
+
+        # To be safe the absolute path of the binary should be used
+        self.binary = os.path.abspath(self.binary)
+
+        # allow Mac binaries to be specified as an app bundle
+        plist = '%s/Contents/Info.plist' % self.binary
+        if mozinfo.isMac and os.path.exists(plist):
+            info = readPlist(plist)
+            self.binary = os.path.join(self.binary, "Contents/MacOS/",
+                                       info['CFBundleExecutable'])
+
+        self.cmdargs = cmdargs or []
+        _cmdargs = [i for i in self.cmdargs
+                    if i != '-foreground']
+        if len(_cmdargs) != len(self.cmdargs):
+            # foreground should be last; see
+            # - https://bugzilla.mozilla.org/show_bug.cgi?id=625614
+            # - https://bugzilla.mozilla.org/show_bug.cgi?id=626826
+            self.cmdargs = _cmdargs
+            self.cmdargs.append('-foreground')
+
+        # process environment
+        if env is None:
+            self.env = os.environ.copy()
+        else:
+            self.env = env.copy()
+        # allows you to run an instance of Firefox separately from any other instances
+        self.env['MOZ_NO_REMOTE'] = '1'
+        # keeps Firefox attached to the terminal window after it starts
+        self.env['NO_EM_RESTART'] = '1'
+
+        # set the library path if needed on linux
+        if sys.platform == 'linux2' and self.binary.endswith('-bin'):
+            dirname = os.path.dirname(self.binary)
+            if os.environ.get('LD_LIBRARY_PATH', None):
+                self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
+            else:
+                self.env['LD_LIBRARY_PATH'] = dirname
+
+    @property
+    def command(self):
+        """Returns the command list to run."""
+        commands = [self.binary, '-profile', self.profile.profile]
+        # Bug 775416 - Ensure that binary options are passed in first
+        commands[1:1] = self.cmdargs
+        return commands
+
+    def get_repositoryInfo(self):
+        """Read repository information from application.ini and platform.ini."""
+
+        config = ConfigParser.RawConfigParser()
+        dirname = os.path.dirname(self.binary)
+        repository = { }
+
+        for file, section in [('application', 'App'), ('platform', 'Build')]:
+            config.read(os.path.join(dirname, '%s.ini' % file))
+
+            for key, id in [('SourceRepository', 'repository'),
+                            ('SourceStamp', 'changeset')]:
+                try:
+                    repository['%s_%s' % (file, id)] = config.get(section, key);
+                except:
+                    repository['%s_%s' % (file, id)] = None
+
+        return repository
+
+
+    def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None):
+        """
+        Run self.command in the proper environment.
+        - debug_args: arguments for the debugger
+        - interactive: uses subprocess.Popen directly
+        - read_output: sends program output to stdout [default=False]
+        - timeout: see process_handler.waitForFinish
+        - outputTimeout: see process_handler.waitForFinish
+        """
+
+        # ensure you are stopped
+        self.stop()
+
+        # ensure the profile exists
+        if not self.profile.exists():
+            self.profile.reset()
+            assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__
+
+        cmd = self._wrap_command(self.command)
+
+        # attach a debugger, if specified
+        if debug_args:
+            cmd = list(debug_args) + cmd
+
+        if interactive:
+            self.process_handler = subprocess.Popen(cmd, env=self.env)
+            # TODO: other arguments
+        else:
+            # this run uses the managed processhandler
+            self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs)
+            self.process_handler.run(timeout, outputTimeout)
+
+
+    def _wrap_command(self, cmd):
+        """
+        If running on OS X 10.5 or older, wrap |cmd| so that it will
+        be executed as an i386 binary, in case it's a 32-bit/64-bit universal
+        binary.
+        """
+        if mozinfo.isMac and hasattr(platform, 'mac_ver') and \
+                               platform.mac_ver()[0][:4] < '10.6':
+            return ["arch", "-arch", "i386"] + cmd
+        return cmd
+
+
+class FirefoxRunner(LocalRunner):
+    """Specialized LocalRunner subclass for running Firefox."""
+
+    profile_class = FirefoxProfile
+
+    def __init__(self, profile, binary=None, **kwargs):
+
+        # take the binary from BROWSER_PATH environment variable
+        binary = binary or os.environ.get('BROWSER_PATH')
+        LocalRunner.__init__(self, profile, binary, **kwargs)
+
+
+class MetroFirefoxRunner(LocalRunner):
+    """Specialized LocalRunner subclass for running Firefox.Metro"""
+
+    profile_class = MetroFirefoxProfile
+
+    # helper application to launch Firefox in Metro mode
+    here = os.path.dirname(os.path.abspath(__file__))
+    immersiveHelperPath = os.path.sep.join([here,
+                                            'resources',
+                                            'metrotestharness.exe'])
+
+    def __init__(self, profile, binary=None, **kwargs):
+
+        # take the binary from BROWSER_PATH environment variable
+        binary = binary or os.environ.get('BROWSER_PATH')
+        LocalRunner.__init__(self, profile, binary, **kwargs)
+
+        if not os.path.exists(self.immersiveHelperPath):
+            raise OSError('Can not find Metro launcher: %s' % self.immersiveHelperPath)
+
+        if not mozinfo.isWin:
+            raise Exception('Firefox Metro mode is only supported on Windows 8 and onwards')
+
+    @property
+    def command(self):
+       command = LocalRunner.command.fget(self)
+       command[:0] = [self.immersiveHelperPath, '-firefoxpath']
+
+       return command
+
+
+class ThunderbirdRunner(LocalRunner):
+    """Specialized LocalRunner subclass for running Thunderbird"""
+    profile_class = ThunderbirdProfile
+
+local_runners = {'firefox': FirefoxRunner,
+                 'metrofirefox' : MetroFirefoxRunner,
+                 'thunderbird': ThunderbirdRunner}
+
+class CLI(MozProfileCLI):
+    """Command line interface."""
+
+    module = "mozrunner"
+
+    def __init__(self, args=sys.argv[1:]):
+        """
+        Setup command line parser and parse arguments
+        - args : command line arguments
+        """
+
+        self.metadata = getattr(sys.modules[self.module],
+                                'package_metadata',
+                                {})
+        version = self.metadata.get('Version')
+        parser_args = {'description': self.metadata.get('Summary')}
+        if version:
+            parser_args['version'] = "%prog " + version
+        self.parser = optparse.OptionParser(**parser_args)
+        self.add_options(self.parser)
+        (self.options, self.args) = self.parser.parse_args(args)
+
+        if getattr(self.options, 'info', None):
+            self.print_metadata()
+            sys.exit(0)
+
+        # choose appropriate runner and profile classes
+        try:
+            self.runner_class = local_runners[self.options.app]
+        except KeyError:
+            self.parser.error('Application "%s" unknown (should be one of "%s")' %
+                              (self.options.app, ', '.join(local_runners.keys())))
+
+    def add_options(self, parser):
+        """add options to the parser"""
+
+        # add profile options
+        MozProfileCLI.add_options(self, parser)
+
+        # add runner options
+        parser.add_option('-b', "--binary",
+                          dest="binary", help="Binary path.",
+                          metavar=None, default=None)
+        parser.add_option('--app', dest='app', default='firefox',
+                          help="Application to use [DEFAULT: %default]")
+        parser.add_option('--app-arg', dest='appArgs',
+                          default=[], action='append',
+                          help="provides an argument to the test application")
+        parser.add_option('--debugger', dest='debugger',
+                          help="run under a debugger, e.g. gdb or valgrind")
+        parser.add_option('--debugger-args', dest='debugger_args',
+                          action='append', default=None,
+                          help="arguments to the debugger")
+        parser.add_option('--interactive', dest='interactive',
+                          action='store_true',
+                          help="run the program interactively")
+        if self.metadata:
+            parser.add_option("--info", dest="info", default=False,
+                              action="store_true",
+                              help="Print module information")
+
+    ### methods for introspecting data
+
+    def get_metadata_from_egg(self):
+        import pkg_resources
+        ret = {}
+        dist = pkg_resources.get_distribution(self.module)
+        if dist.has_metadata("PKG-INFO"):
+            for line in dist.get_metadata_lines("PKG-INFO"):
+                key, value = line.split(':', 1)
+                ret[key] = value
+        if dist.has_metadata("requires.txt"):
+            ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
+        return ret
+
+    def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
+                                   "Author", "Author-email", "License", "Platform", "Dependencies")):
+        for key in data:
+            if key in self.metadata:
+                print key + ": " + self.metadata[key]
+
+    ### methods for running
+
+    def command_args(self):
+        """additional arguments for the mozilla application"""
+        return self.options.appArgs
+
+    def runner_args(self):
+        """arguments to instantiate the runner class"""
+        return dict(cmdargs=self.command_args(),
+                    binary=self.options.binary,
+                    profile_args=self.profile_args())
+
+    def create_runner(self):
+        return self.runner_class.create(**self.runner_args())
+
+    def run(self):
+        runner = self.create_runner()
+        self.start(runner)
+        runner.cleanup()
+
+    def debugger_arguments(self):
+        """
+        returns a 2-tuple of debugger arguments:
+        (debugger_arguments, interactive)
+        """
+        debug_args = self.options.debugger_args
+        interactive = self.options.interactive
+        if self.options.debugger:
+            debug_args, interactive = debugger_arguments(self.options.debugger)
+        return debug_args, interactive
+
+    def start(self, runner):
+        """Starts the runner and waits for Firefox to exit or Keyboard Interrupt.
+        Shoule be overwritten to provide custom running of the runner instance."""
+
+        # attach a debugger if specified
+        debug_args, interactive = self.debugger_arguments()
+        runner.start(debug_args=debug_args, interactive=interactive)
+        print 'Starting:', ' '.join(runner.command)
+        try:
+            runner.wait()
+        except KeyboardInterrupt:
+            runner.stop()
+
+
+def cli(args=sys.argv[1:]):
+    CLI(args).run()
+
+if __name__ == '__main__':
+    cli()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/remote.py
@@ -0,0 +1,335 @@
+import ConfigParser
+import os
+import posixpath
+import re
+import shutil
+import subprocess
+import tempfile
+import time
+import traceback
+
+from runner import Runner
+from mozdevice import DMError
+import mozcrash
+import mozlog
+
+__all__ = ['RemoteRunner', 'B2GRunner', 'remote_runners']
+
+class RemoteRunner(Runner):
+
+    def __init__(self, profile,
+                       devicemanager,
+                       clean_profile=None,
+                       process_class=None,
+                       env=None,
+                       remote_test_root=None,
+                       restore=True):
+
+        super(RemoteRunner, self).__init__(profile, clean_profile=clean_profile,
+                                                process_class=process_class, env=env)
+        self.log = mozlog.getLogger('RemoteRunner')
+
+        self.dm = devicemanager
+        self.remote_test_root = remote_test_root or self.dm.getDeviceRoot()
+        self.remote_profile = posixpath.join(self.remote_test_root, 'profile')
+        self.restore = restore
+        self.backup_files = set([])
+
+    def backup_file(self, remote_path):
+        if not self.restore:
+            return
+
+        if self.dm.fileExists(remote_path):
+            self.dm.shellCheckOutput(['dd', 'if=%s' % remote_path, 'of=%s.orig' % remote_path])
+            self.backup_files.add(remote_path)
+
+
+    def check_for_crashes(self, symbols_path, last_test=None):
+        crashed = False
+        remote_dump_dir = posixpath.join(self.remote_profile, 'minidumps')
+        self.log.info("checking for crashes in '%s'" % remote_dump_dir)
+        if self.dm.dirExists(remote_dump_dir):
+            local_dump_dir = tempfile.mkdtemp()
+            self.dm.getDirectory(remote_dump_dir, local_dump_dir)
+            try:
+                crashed = mozcrash.check_for_crashes(local_dump_dir, symbols_path, test_name=last_test)
+            except:
+                traceback.print_exc()
+            finally:
+                shutil.rmtree(local_dump_dir)
+                self.dm.removeDir(remote_dump_dir)
+        return crashed
+
+    def cleanup(self):
+        if not self.restore:
+            return
+
+        super(RemoteRunner, self).cleanup()
+
+        for backup_file in self.backup_files:
+            # Restore the original profiles.ini
+            self.dm.shellCheckOutput(['dd', 'if=%s.orig' % backup_file, 'of=%s' % backup_file])
+            self.dm.removeFile("%s.orig" % backup_file)
+
+        # Delete any bundled extensions
+        extension_dir = posixpath.join(self.remote_profile, 'extensions', 'staged')
+        if self.dm.dirExists(extension_dir):
+            for filename in self.dm.listFiles(extension_dir):
+                try:
+                    self.dm.removeDir(posixpath.join(self.bundles_dir, filename))
+                except DMError:
+                    pass
+        # Remove the test profile
+        self.dm.removeDir(self.remote_profile)
+
+class B2GRunner(RemoteRunner):
+
+    def __init__(self, profile, devicemanager, marionette, context_chrome=True,
+                 test_script=None, test_script_args=None, **kwargs):
+
+        super(B2GRunner, self).__init__(profile, devicemanager, **kwargs)
+        self.log = mozlog.getLogger('B2GRunner')
+
+        tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
+        os.close(tmpfd)
+        tmp_env = self.env or {}
+        self.env = { 'MOZ_CRASHREPORTER': '1',
+                     'MOZ_CRASHREPORTER_NO_REPORT': '1',
+                     'MOZ_HIDE_RESULTS_TABLE': '1',
+                     'MOZ_PROCESS_LOG': processLog,
+                     'NO_EM_RESTART': '1', }
+        self.env.update(tmp_env)
+        self.last_test = "automation"
+        self.marionette = marionette
+        self.context_chrome = context_chrome
+        self.test_script = test_script
+        self.test_script_args = test_script_args
+        self.remote_profiles_ini = '/data/b2g/mozilla/profiles.ini'
+        self.bundles_dir = '/system/b2g/distribution/bundles'
+        self.user_js = '/data/local/user.js'
+
+    @property
+    def command(self):
+        cmd = [self.dm._adbPath]
+        if self.dm._deviceSerial:
+            cmd.extend(['-s', self.dm._deviceSerial])
+        cmd.append('shell')
+        for k, v in self.env.iteritems():
+            cmd.append("%s=%s" % (k, v))
+        cmd.append('/system/bin/b2g.sh')
+        return cmd
+
+    def start(self, timeout=None, outputTimeout=None):
+        self.timeout = timeout
+        self.outputTimeout = outputTimeout
+        self._setup_remote_profile()
+        # reboot device so it starts up with the proper profile
+        if not self.marionette.emulator:
+            self._reboot_device()
+            #wait for wlan to come up
+            if not self._wait_for_net():
+                raise Exception("network did not come up, please configure the network" +
+                                " prior to running before running the automation framework")
+
+        self.dm.shellCheckOutput(['stop', 'b2g'])
+
+        self.kp_kwargs['processOutputLine'] = [self.on_output]
+        self.kp_kwargs['onTimeout'] = [self.on_timeout]
+        self.process_handler = self.process_class(self.command, **self.kp_kwargs)
+        self.process_handler.run(timeout=timeout, outputTimeout=outputTimeout)
+
+        # Set up port forwarding again for Marionette, since any that
+        # existed previously got wiped out by the reboot.
+        if not self.marionette.emulator:
+            subprocess.Popen([self.dm._adbPath,
+                              'forward',
+                              'tcp:%s' % self.marionette.port,
+                              'tcp:%s' % self.marionette.port]).communicate()
+        self.marionette.wait_for_port()
+
+        # start a marionette session
+        session = self.marionette.start_session()
+        if 'b2g' not in session:
+            raise Exception("bad session value %s returned by start_session" % session)
+
+        if self.marionette.emulator:
+            # Disable offline status management (bug 777145), otherwise the network
+            # will be 'offline' when the mochitests start.  Presumably, the network
+            # won't be offline on a real device, so we only do this for emulators.
+            self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+            self.marionette.execute_script("""
+                Components.utils.import("resource://gre/modules/Services.jsm");
+                Services.io.manageOfflineStatus = false;
+                Services.io.offline = false;
+                """)
+
+        if self.context_chrome:
+            self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+        else:
+            self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
+
+        # run the script that starts the tests
+
+        if self.test_script:
+            if os.path.isfile(self.test_script):
+                script = open(self.test_script, 'r')
+                self.marionette.execute_script(script.read(), script_args=self.test_script_args)
+                script.close()
+            elif isinstance(self.test_script, basestring):
+                self.marionette.execute_script(self.test_script, script_args=self.test_script_args)
+        else:
+            # assumes the tests are started on startup automatically
+            pass
+
+    def on_output(self, line):
+        print line
+        match = re.findall(r"TEST-START \| ([^\s]*)", line)
+        if match:
+            self.last_test = match[-1]
+
+    def on_timeout(self):
+        self.log.testFail("%s | application timed "
+                         "out after %s seconds with no output",
+                         self.last_test, self.timeout)
+
+    def _reboot_device(self):
+        serial, status = self._get_device_status()
+        self.dm.shellCheckOutput(['/system/bin/reboot'])
+
+        # The reboot command can return while adb still thinks the device is
+        # connected, so wait a little bit for it to disconnect from adb.
+        time.sleep(10)
+
+        # wait for device to come back to previous status
+        self.log.info('waiting for device to come back online after reboot')
+        start = time.time()
+        rserial, rstatus = self._get_device_status(serial)
+        while rstatus != 'device':
+            if time.time() - start > 120:
+                # device hasn't come back online in 2 minutes, something's wrong
+                raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus))
+            time.sleep(5)
+            rserial, rstatus = self.getDeviceStatus(serial)
+        self.log.info('device:', serial, 'status:', rstatus)
+
+    def _get_device_status(self, serial=None):
+        # If we know the device serial number, we look for that,
+        # otherwise we use the (presumably only) device shown in 'adb devices'.
+        serial = serial or self.dm._deviceSerial
+        status = 'unknown'
+
+        proc = subprocess.Popen([self.dm._adbPath, 'devices'], stdout=subprocess.PIPE)
+        line = proc.stdout.readline()
+        while line != '':
+            result = re.match('(.*?)\t(.*)', line)
+            if result:
+                thisSerial = result.group(1)
+                if not serial or thisSerial == serial:
+                    serial = thisSerial
+                    status = result.group(2)
+                    break
+            line = proc.stdout.readline()
+        return (serial, status)
+
+    def _wait_for_net(self):
+        active = False
+        time_out = 0
+        while not active and time_out < 40:
+            proc = subprocess.Popen([self.dm._adbPath, 'shell', '/system/bin/netcfg'])
+            proc.stdout.readline() # ignore first line
+            line = proc.stdout.readline()
+            while line != "":
+                if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)):
+                    active = True
+                    break
+                line = proc.stdout.readline()
+            time_out += 1
+            time.sleep(1)
+        return active
+
+    def _setup_remote_profile(self):
+        """
+        Copy profile and update the remote profiles ini file to point to the new profile
+        """
+
+        # copy the profile to the device.
+        if self.dm.dirExists(self.remote_profile):
+            self.dm.shellCheckOutput(['rm', '-r', self.remote_profile])
+
+        try:
+            self.dm.pushDir(self.profile.profile, self.remote_profile)
+        except DMError:
+            self.log.error("Automation Error: Unable to copy profile to device.")
+            raise
+
+        extension_dir = os.path.join(self.profile.profile, 'extensions', 'staged')
+        if os.path.isdir(extension_dir):
+            # Copy the extensions to the B2G bundles dir.
+            # need to write to read-only dir
+            subprocess.Popen([self.dm._adbPath, 'remount']).communicate()
+            for filename in os.listdir(extension_dir):
+                self.dm.shellCheckOutput(['rm', '-rf',
+                                          os.path.join(self.bundles_dir, filename)])
+            try:
+                self.dm.pushDir(extension_dir, self.bundles_dir)
+            except DMError:
+                self.log.error("Automation Error: Unable to copy extensions to device.")
+                raise
+
+        if not self.dm.fileExists(self.remote_profiles_ini):
+            raise DMError("The profiles.ini file '%s' does not exist on the device" % self.remote_profiles_ini)
+
+        local_profiles_ini = tempfile.NamedTemporaryFile()
+        self.dm.getFile(self.remote_profiles_ini, local_profiles_ini.name)
+
+        config = ProfileConfigParser()
+        config.read(local_profiles_ini.name)
+        for section in config.sections():
+            if 'Profile' in section:
+                config.set(section, 'IsRelative', 0)
+                config.set(section, 'Path', self.remote_profile)
+
+        new_profiles_ini = tempfile.NamedTemporaryFile()
+        config.write(open(new_profiles_ini.name, 'w'))
+
+        self.backup_file(self.remote_profiles_ini)
+        self.dm.pushFile(new_profiles_ini.name, self.remote_profiles_ini)
+
+        # 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.backup_file(self.user_js)
+        self.dm.pushFile(os.path.join(self.profile.profile, "user.js"), self.user_js)
+
+    def cleanup(self):
+        super(B2GRunner, self).cleanup()
+        if getattr(self.marionette, 'instance', False):
+            self.marionette.instance.close()
+        del self.marionette
+
+class ProfileConfigParser(ConfigParser.RawConfigParser):
+    """Subclass of RawConfigParser that outputs .ini files in the exact
+       format expected for profiles.ini, which is slightly different
+       than the default format.
+    """
+
+    def optionxform(self, optionstr):
+        return optionstr
+
+    def write(self, fp):
+        if self._defaults:
+            fp.write("[%s]\n" % ConfigParser.DEFAULTSECT)
+            for (key, value) in self._defaults.items():
+                fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t')))
+            fp.write("\n")
+        for section in self._sections:
+            fp.write("[%s]\n" % section)
+            for (key, value) in self._sections[section].items():
+                if key == "__name__":
+                    continue
+                if (value is not None) or (self._optcre == self.OPTCRE):
+                    key = "=".join((key, str(value).replace('\n', '\n\t')))
+                fp.write("%s\n" % (key))
+            fp.write("\n")
+
+remote_runners = {'b2g': 'B2GRunner',
+                  'fennec': 'FennecRunner'}
new file mode 100644
index 0000000000000000000000000000000000000000..d3bcbfbee9af5f97a6af47287f0f6e22438774bb
GIT binary patch
literal 63488
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%0DY{$?Q00
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
P`u+O-`u+O-!r%V@N-O$w
--- a/testing/mozbase/mozrunner/mozrunner/runner.py
+++ b/testing/mozbase/mozrunner/mozrunner/runner.py
@@ -1,388 +1,113 @@
 #!/usr/bin/env python
 
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
-
-__all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'runners', 'CLI', 'cli', 'package_metadata']
+import subprocess
 
-import mozinfo
-import optparse
-import os
-import platform
-import subprocess
-import sys
-import ConfigParser
-
-from utils import get_metadata_from_egg
-from utils import findInPath
-from mozprofile import *
 from mozprocess.processhandler import ProcessHandler
-
-if mozinfo.isMac:
-    from plistlib import readPlist
-
-package_metadata = get_metadata_from_egg('mozrunner')
+import mozlog
 
-# Map of debugging programs to information about them
-# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59
-debuggers = {'gdb': {'interactive': True,
-                     'args': ['-q', '--args'],},
-             'valgrind': {'interactive': False,
-                          'args': ['--leak-check=full']}
-             }
-
-def debugger_arguments(debugger, arguments=None, interactive=None):
-    """
-    finds debugger arguments from debugger given and defaults
-    * debugger : debugger name or path to debugger
-    * arguments : arguments to the debugger, or None to use defaults
-    * interactive : whether the debugger should be run in interactive mode, or None to use default
-    """
-
-    # find debugger executable if not a file
-    executable = debugger
-    if not os.path.exists(executable):
-        executable = findInPath(debugger)
-    if executable is None:
-        raise Exception("Path to '%s' not found" % debugger)
-
-    # if debugger not in dictionary of knowns return defaults
-    dirname, debugger = os.path.split(debugger)
-    if debugger not in debuggers:
-        return ([executable] + (arguments or []), bool(interactive))
-
-    # otherwise use the dictionary values for arguments unless specified
-    if arguments is None:
-        arguments = debuggers[debugger].get('args', [])
-    if interactive is None:
-        interactive = debuggers[debugger].get('interactive', False)
-    return ([executable] + arguments, interactive)
+# we can replace this method with 'abc'
+# (http://docs.python.org/library/abc.html) when we require Python 2.6+
+def abstractmethod(method):
+  line = method.func_code.co_firstlineno
+  filename = method.func_code.co_filename
+  def not_implemented(*args, **kwargs):
+    raise NotImplementedError('Abstract method %s at File "%s", line %s '
+                              'should be implemented by a concrete class' %
+                              (repr(method), filename, line))
+  return not_implemented
 
 class Runner(object):
-    """Handles all running operations. Finds bins, runs and kills the process."""
-
-    profile_class = Profile # profile class to use by default
-
-    @classmethod
-    def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None,
-               clean_profile=True, process_class=ProcessHandler):
-        profile = cls.profile_class(**(profile_args or {}))
-        return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs,
-                                           clean_profile=clean_profile, process_class=process_class)
-
-    def __init__(self, profile, binary, cmdargs=None, env=None,
-                 kp_kwargs=None, clean_profile=True, process_class=ProcessHandler):
-        self.process_handler = None
-        self.process_class = process_class
-        self.profile = profile
-        self.clean_profile = clean_profile
-
-        # find the binary
-        self.binary = binary
-        if not self.binary:
-            raise Exception("Binary not specified")
-        if not os.path.exists(self.binary):
-            raise OSError("Binary path does not exist: %s" % self.binary)
-
-        # allow Mac binaries to be specified as an app bundle
-        plist = '%s/Contents/Info.plist' % self.binary
-        if mozinfo.isMac and os.path.exists(plist):
-            info = readPlist(plist)
-            self.binary = os.path.join(self.binary, "Contents/MacOS/",
-                                       info['CFBundleExecutable'])
-
-        self.cmdargs = cmdargs or []
-        _cmdargs = [i for i in self.cmdargs
-                    if i != '-foreground']
-        if len(_cmdargs) != len(self.cmdargs):
-            # foreground should be last; see
-            # - https://bugzilla.mozilla.org/show_bug.cgi?id=625614
-            # - https://bugzilla.mozilla.org/show_bug.cgi?id=626826
-            self.cmdargs = _cmdargs
-            self.cmdargs.append('-foreground')
 
-        # process environment
-        if env is None:
-            self.env = os.environ.copy()
-        else:
-            self.env = env.copy()
-        # allows you to run an instance of Firefox separately from any other instances
-        self.env['MOZ_NO_REMOTE'] = '1'
-        # keeps Firefox attached to the terminal window after it starts
-        self.env['NO_EM_RESTART'] = '1'
-
-        # set the library path if needed on linux
-        if sys.platform == 'linux2' and self.binary.endswith('-bin'):
-            dirname = os.path.dirname(self.binary)
-            if os.environ.get('LD_LIBRARY_PATH', None):
-                self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
-            else:
-                self.env['LD_LIBRARY_PATH'] = dirname
-
-        # arguments for ProfessHandler.Process
+    def __init__(self, profile, clean_profile=True, process_class=None, kp_kwargs=None, env=None):
+        self.clean_profile = clean_profile
+        self.env = env or {}
         self.kp_kwargs = kp_kwargs or {}
-
-    @property
-    def command(self):
-        """Returns the command list to run."""
-        return [self.binary, '-profile', self.profile.profile]
+        self.process_class = process_class or ProcessHandler
+        self.process_handler = None
+        self.profile = profile
+        self.log = mozlog.getLogger('MozRunner')
 
-    def get_repositoryInfo(self):
-        """Read repository information from application.ini and platform.ini."""
-
-        config = ConfigParser.RawConfigParser()
-        dirname = os.path.dirname(self.binary)
-        repository = { }
-
-        for file, section in [('application', 'App'), ('platform', 'Build')]:
-            config.read(os.path.join(dirname, '%s.ini' % file))
-
-            for key, id in [('SourceRepository', 'repository'),
-                            ('SourceStamp', 'changeset')]:
-                try:
-                    repository['%s_%s' % (file, id)] = config.get(section, key);
-                except:
-                    repository['%s_%s' % (file, id)] = None
-
-        return repository
-
-    def is_running(self):
-        return self.process_handler is not None
-
-    def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None):
+    @abstractmethod
+    def start(self, *args, **kwargs):
         """
-        Run self.command in the proper environment.
-        - debug_args: arguments for the debugger
-        - interactive: uses subprocess.Popen directly
-        - read_output: sends program output to stdout [default=False]
-        - timeout: see process_handler.waitForFinish
-        - outputTimeout: see process_handler.waitForFinish
+        Run the process
         """
 
         # ensure you are stopped
         self.stop()
 
         # ensure the profile exists
         if not self.profile.exists():
             self.profile.reset()
             assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__
 
-        cmd = self._wrap_command(self.command+self.cmdargs)
+        cmd = self._wrap_command(self.command)
 
         # attach a debugger, if specified
         if debug_args:
             cmd = list(debug_args) + cmd
 
         if interactive:
             self.process_handler = subprocess.Popen(cmd, env=self.env)
             # TODO: other arguments
         else:
             # this run uses the managed processhandler
             self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs)
             self.process_handler.run(timeout, outputTimeout)
 
     def wait(self, timeout=None):
         """
-        Wait for the app to exit.
+        Wait for the process to exit.
 
         If timeout is not None, will return after timeout seconds.
         Use is_running() to determine whether or not a timeout occured.
         Timeout is ignored if interactive was set to True.
         """
         if self.process_handler is None:
             return
 
         if isinstance(self.process_handler, subprocess.Popen):
             self.process_handler.wait()
         else:
             self.process_handler.wait(timeout)
             if self.process_handler.proc.poll() is None:
-                # waitForFinish timed out
+                # wait timed out
                 return
-
         self.process_handler = None
 
+    def is_running(self):
+        """
+        Returns True if the process is still running, False otherwise
+        """
+        return self.process_handler is not None
+
+
     def stop(self):
-        """Kill the app"""
+        """
+        Kill the process
+        """
         if self.process_handler is None:
             return
         self.process_handler.kill()
         self.process_handler = None
 
     def reset(self):
         """
-        reset the runner between runs
-        currently, only resets the profile, but probably should do more
+        Reset the runner to its default state
         """
-        self.profile.reset()
+        if getattr(self, 'profile', False):
+            self.profile.reset()
 
     def cleanup(self):
-        self.stop()
-        if self.clean_profile:
-            self.profile.cleanup()
-
-    def _wrap_command(self, cmd):
+        """
+        Cleanup all runner state
         """
-        If running on OS X 10.5 or older, wrap |cmd| so that it will
-        be executed as an i386 binary, in case it's a 32-bit/64-bit universal
-        binary.
-        """
-        if mozinfo.isMac and hasattr(platform, 'mac_ver') and \
-                               platform.mac_ver()[0][:4] < '10.6':
-            return ["arch", "-arch", "i386"] + cmd
-        return cmd
+        if self.is_running():
+            self.stop()
+        if getattr(self, 'profile', False) and self.clean_profile:
+            self.profile.cleanup()
 
     __del__ = cleanup
-
-
-class FirefoxRunner(Runner):
-    """Specialized Runner subclass for running Firefox."""
-
-    profile_class = FirefoxProfile
-
-    def __init__(self, profile, binary=None, **kwargs):
-
-        # take the binary from BROWSER_PATH environment variable
-        if (not binary) and 'BROWSER_PATH' in os.environ:
-            binary = os.environ['BROWSER_PATH']
-
-        Runner.__init__(self, profile, binary, **kwargs)
-
-class ThunderbirdRunner(Runner):
-    """Specialized Runner subclass for running Thunderbird"""
-    profile_class = ThunderbirdProfile
-
-runners = {'firefox': FirefoxRunner,
-           'thunderbird': ThunderbirdRunner}
-
-class CLI(MozProfileCLI):
-    """Command line interface."""
-
-    module = "mozrunner"
-
-    def __init__(self, args=sys.argv[1:]):
-        """
-        Setup command line parser and parse arguments
-        - args : command line arguments
-        """
-
-        self.metadata = getattr(sys.modules[self.module],
-                                'package_metadata',
-                                {})
-        version = self.metadata.get('Version')
-        parser_args = {'description': self.metadata.get('Summary')}
-        if version:
-            parser_args['version'] = "%prog " + version
-        self.parser = optparse.OptionParser(**parser_args)
-        self.add_options(self.parser)
-        (self.options, self.args) = self.parser.parse_args(args)
-
-        if getattr(self.options, 'info', None):
-            self.print_metadata()
-            sys.exit(0)
-
-        # choose appropriate runner and profile classes
-        try:
-            self.runner_class = runners[self.options.app]
-        except KeyError:
-            self.parser.error('Application "%s" unknown (should be one of "firefox" or "thunderbird")' % self.options.app)
-
-    def add_options(self, parser):
-        """add options to the parser"""
-
-        # add profile options
-        MozProfileCLI.add_options(self, parser)
-
-        # add runner options
-        parser.add_option('-b', "--binary",
-                          dest="binary", help="Binary path.",
-                          metavar=None, default=None)
-        parser.add_option('--app', dest='app', default='firefox',
-                          help="Application to use [DEFAULT: %default]")
-        parser.add_option('--app-arg', dest='appArgs',
-                          default=[], action='append',
-                          help="provides an argument to the test application")
-        parser.add_option('--debugger', dest='debugger',
-                          help="run under a debugger, e.g. gdb or valgrind")
-        parser.add_option('--debugger-args', dest='debugger_args',
-                          action='append', default=None,
-                          help="arguments to the debugger")
-        parser.add_option('--interactive', dest='interactive',
-                          action='store_true',
-                          help="run the program interactively")
-        if self.metadata:
-            parser.add_option("--info", dest="info", default=False,
-                              action="store_true",
-                              help="Print module information")
-
-    ### methods for introspecting data
-
-    def get_metadata_from_egg(self):
-        import pkg_resources
-        ret = {}
-        dist = pkg_resources.get_distribution(self.module)
-        if dist.has_metadata("PKG-INFO"):
-            for line in dist.get_metadata_lines("PKG-INFO"):
-                key, value = line.split(':', 1)
-                ret[key] = value
-        if dist.has_metadata("requires.txt"):
-            ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
-        return ret
-
-    def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
-                                   "Author", "Author-email", "License", "Platform", "Dependencies")):
-        for key in data:
-            if key in self.metadata:
-                print key + ": " + self.metadata[key]
-
-    ### methods for running
-
-    def command_args(self):
-        """additional arguments for the mozilla application"""
-        return self.options.appArgs
-
-    def runner_args(self):
-        """arguments to instantiate the runner class"""
-        return dict(cmdargs=self.command_args(),
-                    binary=self.options.binary,
-                    profile_args=self.profile_args())
-
-    def create_runner(self):
-        return self.runner_class.create(**self.runner_args())
-
-    def run(self):
-        runner = self.create_runner()
-        self.start(runner)
-        runner.cleanup()
-
-    def debugger_arguments(self):
-        """
-        returns a 2-tuple of debugger arguments:
-        (debugger_arguments, interactive)
-        """
-        debug_args = self.options.debugger_args
-        interactive = self.options.interactive
-        if self.options.debugger:
-            debug_args, interactive = debugger_arguments(self.options.debugger)
-        return debug_args, interactive
-
-    def start(self, runner):
-        """Starts the runner and waits for Firefox to exit or Keyboard Interrupt.
-        Shoule be overwritten to provide custom running of the runner instance."""
-
-        # attach a debugger if specified
-        debug_args, interactive = self.debugger_arguments()
-        runner.start(debug_args=debug_args, interactive=interactive)
-        print 'Starting:', ' '.join(runner.command)
-        try:
-            runner.wait()
-        except KeyboardInterrupt:
-            runner.stop()
-
-
-def cli(args=sys.argv[1:]):
-    CLI(args).run()
-
-if __name__ == '__main__':
-    cli()
--- a/testing/mozbase/mozrunner/setup.py
+++ b/testing/mozbase/mozrunner/setup.py
@@ -1,23 +1,26 @@
 # 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 sys
 from setuptools import setup
 
 PACKAGE_NAME = "mozrunner"
-PACKAGE_VERSION = '5.15'
+PACKAGE_VERSION = '5.22'
 
 desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
 
-deps = ['mozinfo >= 0.4',
+deps = ['mozcrash >= 0.3',
+        'mozdevice >= 0.28',
+        'mozinfo >= 0.4',
+        'mozlog >= 1.3',
         'mozprocess >= 0.8',
-        'mozprofile >= 0.4',
+        'mozprofile >= 0.11',
        ]
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
       description=desc,
@@ -31,16 +34,19 @@ setup(name=PACKAGE_NAME,
                    'Topic :: Software Development :: Libraries :: Python Modules',
                    ],
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
       url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL 2.0',
       packages=['mozrunner'],
+      package_data={'mozrunner': [
+            'resources/metrotestharness.exe'
+      ]},
       zip_safe=False,
       install_requires = deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       mozrunner = mozrunner:cli
       """,
     )
deleted file mode 100644
--- a/testing/mozbase/moztest/README.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# Moztest
-
-Package for handling Mozilla test results.
-
-
-## Usage example
-
-This shows how you can create an xUnit representation of python unittest results.
-
-    from results import TestResultCollection
-    from output import XUnitOutput
-
-    collection = TestResultCollection.from_unittest_results(results)
-    out = XUnitOutput()
-    with open('out.xml', 'w') as f:
-        out.serialize(collection, f)
--- a/testing/mozbase/moztest/setup.py
+++ b/testing/mozbase/moztest/setup.py
@@ -1,39 +1,30 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
-
-import os
 from setuptools import setup
 
-PACKAGE_VERSION = '0.1'
-
-# 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 = ''
+PACKAGE_VERSION = '0.2'
 
 # dependencies
 deps = ['mozinfo']
 try:
     import json
 except ImportError:
     deps.append('simplejson')
 
 setup(name='moztest',
       version=PACKAGE_VERSION,
       description="Package for storing and outputting Mozilla test results",
-      long_description=description,
+      long_description="see http://mozbase.readthedocs.org/",
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Mozilla Automation and Tools team',
       author_email='tools@lists.mozilla.org',
-      url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
       license='MPL',
       packages=['moztest'],
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       )
--- a/testing/mozbase/setup_development.py
+++ b/testing/mozbase/setup_development.py
@@ -5,20 +5,19 @@
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 """
 Setup mozbase packages for development.
 
 Packages may be specified as command line arguments.
 If no arguments are given, install all packages.
 
-See https://wiki.mozilla.org/Auto-tools/Projects/MozBase
+See https://wiki.mozilla.org/Auto-tools/Projects/Mozbase
 """
 
-import pkg_resources
 import os
 import subprocess
 import sys
 from optparse import OptionParser
 from subprocess import PIPE
 try:
     from subprocess import check_call as call
 except ImportError:
@@ -26,17 +25,19 @@ except ImportError:
 
 
 # directory containing this file
 here = os.path.dirname(os.path.abspath(__file__))
 
 # all python packages
 mozbase_packages = [i for i in os.listdir(here)
                     if os.path.exists(os.path.join(here, i, 'setup.py'))]
-extra_packages = ["sphinx"]
+extra_packages = ["sphinx", # documentation: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Documentation
+                  "mock",   # testing: https://wiki.mozilla.org/Auto-tools/Projects/Mozbase#Tests
+                  ]
 
 def cycle_check(order, dependencies):
     """ensure no cyclic dependencies"""
     order_dict = dict([(j, i) for i, j in enumerate(order)])
     for package, deps in dependencies.items():
         index = order_dict[package]
         for d in deps:
             assert index > order_dict[d], "Cyclic dependencies detected"
@@ -223,16 +224,23 @@ def main(args=sys.argv[1:]):
             print package
         parser.exit()
 
     # set up the packages for development
     for package in unrolled:
         call([sys.executable, 'setup.py', 'develop', '--no-deps'],
              cwd=os.path.join(here, reverse_mapping[package]))
 
+    # add the directory of sys.executable to path to aid the correct
+    # `easy_install` getting called
+    # https://bugzilla.mozilla.org/show_bug.cgi?id=893878
+    os.environ['PATH'] = '%s%s%s' % (os.path.dirname(os.path.abspath(sys.executable)),
+                                     os.path.pathsep,
+                                     os.environ.get('PATH', '').strip(os.path.pathsep))
+
     # install non-mozbase dependencies
     # these need to be installed separately and the --no-deps flag
     # subsequently used due to a bug in setuptools; see
     # https://bugzilla.mozilla.org/show_bug.cgi?id=759836
     pypi_deps = dict([(i, j) for i,j in alldeps.items()
                       if i not in unrolled])
     for package, version in pypi_deps.items():
         # easy_install should be available since we rely on setuptools
--- a/testing/mozbase/test-manifest.ini
+++ b/testing/mozbase/test-manifest.ini
@@ -8,11 +8,15 @@
 # run with
 # https://github.com/mozilla/mozbase/blob/master/test.py
 
 [include:manifestdestiny/tests/manifest.ini]
 [include:mozcrash/tests/manifest.ini]
 [include:mozdevice/tests/manifest.ini]
 [include:mozfile/tests/manifest.ini]
 [include:mozhttpd/tests/manifest.ini]
+[include:mozinfo/tests/manifest.ini]
+[include:mozinstall/tests/manifest.ini]
+[include:mozlog/tests/manifest.ini]
 [include:mozprocess/tests/manifest.ini]
 [include:mozprofile/tests/manifest.ini]
 [include:moztest/tests/manifest.ini]
+[include:moznetwork/tests/manifest.ini]