Bug 706844 - Create a make target for peptest. r=jmaher
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Tue, 06 Dec 2011 09:26:24 -0500
changeset 82071 65c05ff60e47d68eebe82705b389c07ece2bdfcd
parent 82070 9a59028a35108dc809341ed9ecf9c71ba6e1538b
child 82072 3204b70435fe8a83ff33cbaef12e33ab3ebe3b2b
push id21582
push userbmo@edmorley.co.uk
push dateWed, 07 Dec 2011 09:30:09 +0000
treeherdermozilla-central@489f2d51b011 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjmaher
bugs706844
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 706844 - Create a make target for peptest. r=jmaher
testing/mozbase/Makefile.in
testing/mozbase/manifestdestiny/README.md
testing/mozbase/manifestdestiny/README.txt
testing/mozbase/manifestdestiny/manifestparser.py
testing/mozbase/manifestdestiny/manifestparser/__init__.py
testing/mozbase/manifestdestiny/manifestparser/manifestparser.py
testing/mozbase/manifestdestiny/setup.py
testing/mozbase/manifestdestiny/tests/test.py
testing/mozbase/mozdevice/README.md
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/mozhttpd/mozhttpd.py
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/mozinfo/mozinfo.py
testing/mozbase/mozinfo/mozinfo/__init__.py
testing/mozbase/mozinfo/mozinfo/mozinfo.py
testing/mozbase/mozinfo/setup.py
testing/mozbase/mozinstall/mozinstall.py
testing/mozbase/mozinstall/mozinstall/__init__.py
testing/mozbase/mozinstall/mozinstall/mozinstall.py
testing/mozbase/mozinstall/setup.py
testing/mozbase/mozprocess/mozprocess/pid.py
testing/mozbase/mozprofile/setup.py
testing/mozbase/mozrunner/mozrunner/utils.py
testing/mozbase/mozrunner/setup.py
testing/mozbase/setup_development.py
testing/peptest/Makefile.in
testing/peptest/runtests.py
testing/testsuite-targets.mk
--- a/testing/mozbase/Makefile.in
+++ b/testing/mozbase/Makefile.in
@@ -61,13 +61,19 @@ MOZBASE_PACKAGES = \
   mozrunner \
   $(NULL)
 
 MOZBASE_EXTRAS = \
   setup_development.py \
   README \
   $(NULL)
 
+_DEST_DIR = $(DEPTH)/_tests/mozbase
+libs:: $(MOZBASE_PACKAGES)
+	$(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR)
+libs:: $(MOZBASE_EXTRAS)
+	$(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR)
+
 stage-package: PKG_STAGE = $(DIST)/test-package-stage
 stage-package:
 	$(NSINSTALL) -D $(PKG_STAGE)/mozbase
 	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(MOZBASE_PACKAGES)) | (cd $(PKG_STAGE)/mozbase && tar -xf -)
 	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(MOZBASE_EXTRAS)) | (cd $(PKG_STAGE)/mozbase && tar -xf -)
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/README.md
@@ -0,0 +1,325 @@
+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:
+
+   [{'path':
+     '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests/testToolbar/testBackForwardButtons.js',
+     'name': 'testToolbar/testBackForwardButtons.js', 'here':
+     '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests',
+     'manifest': '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests',}]
+
+The keys displayed here (path, 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?
+
+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
+* name: short name of the test; this is the (usually) relative path
+  specified in the section name
+* 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 ...
+
+
+# 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.
+
+
+# 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
deleted file mode 100644
--- a/testing/mozbase/manifestdestiny/README.txt
+++ /dev/null
@@ -1,345 +0,0 @@
-ManifestDestiny
-===============
-
-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::
-
-  [{'path':
-    '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests/testToolbar/testBackForwardButtons.js',
-    'name': 'testToolbar/testBackForwardButtons.js', 'here':
-    '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests',
-    'manifest': '/home/jhammel/mozmill/src/ManifestDestiny/manifestdestiny/tests',}]
-
-The keys displayed here (path, 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?
-------------------------
-
-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
-* name: short name of the test; this is the (usually) relative path
-  specified in the section name
-* 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 http://hg.mozilla.org/automation/ManifestDestiny
-and
-http://hg.mozilla.org/automation/ManifestDestiny/file/tip/manifestparser.py
-in particular.
-
-
-Using Manifests
----------------
-
-A test harness will normally call ``TestManifest.active_tests`` (
-http://hg.mozilla.org/automation/ManifestDestiny/file/c0399fbfa830/manifestparser.py#l506 )::
-
-   506     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.${TAG}`` or
-``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 ...
-
-
-Tests
------
-
-ManifestDestiny includes a suite of tests:
-
-http://hg.mozilla.org/automation/ManifestDestiny/file/tip/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. 
-
-
-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
deleted file mode 100644
--- a/testing/mozbase/manifestdestiny/manifestparser.py
+++ /dev/null
@@ -1,1114 +0,0 @@
-#!/usr/bin/env python
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is manifestdestiny.
-#
-# The Initial Developer of the Original Code is
-#  The Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2010
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#     Jeff Hammel <jhammel@mozilla.com>     (Original author)
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either of the GNU General Public License Version 2 or later (the "GPL"),
-# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-
-"""
-Mozilla universal manifest parser
-"""
-
-# this file lives at
-# http://hg.mozilla.org/automation/ManifestDestiny/raw-file/tip/manifestparser.py
-
-__all__ = ['read_ini', # .ini reader
-           'ManifestParser', 'TestManifest', 'convert', # manifest handling
-           'parse', 'ParseError', 'ExpressionParser'] # conditional expression parser
-
-import os
-import re
-import shutil
-import sys
-from fnmatch import fnmatch
-from optparse import OptionParser
-
-version = '0.5.4' # package version
-try:
-    from setuptools import setup
-except:
-    setup = None
-
-# 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 start
-        return os.path.join(*rel_list)
-
-# 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
-#
-# Rough grammar:
-# expr := literal
-#       | '(' expr ')'
-#       | expr '&&' expr
-#       | expr '||' expr
-#       | expr '==' expr
-#       | expr '!=' expr
-# literal := BOOL
-#          | INT
-#          | STRING
-#          | IDENT
-# BOOL   := true|false
-# INT    := [0-9]+
-# STRING := "[^"]*"
-# IDENT  := [A-Za-z_]\w*
-
-# Identifiers take their values from a mapping dictionary passed as the second
-# argument.
-
-# Glossary (see above URL for details):
-# - nud: null denotation
-# - led: left detonation
-# - lbp: left binding power
-# - rbp: right binding power
-
-class ident_token(object):
-    def __init__(self, value):
-        self.value = value
-    def nud(self, parser):
-        # identifiers take their value from the value mappings passed
-        # to the parser
-        return parser.value(self.value)
-
-class literal_token(object):
-    def __init__(self, value):
-        self.value = value
-    def nud(self, parser):
-        return self.value
-
-class eq_op_token(object):
-    "=="
-    def led(self, parser, left):
-        return left == parser.expression(self.lbp)
-
-class neq_op_token(object):
-    "!="
-    def led(self, parser, left):
-        return left != parser.expression(self.lbp)
-
-class not_op_token(object):
-    "!"
-    def nud(self, parser):
-        return not parser.expression()
-
-class and_op_token(object):
-    "&&"
-    def led(self, parser, left):
-        right = parser.expression(self.lbp)
-        return left and right
-
-class or_op_token(object):
-    "||"
-    def led(self, parser, left):
-        right = parser.expression(self.lbp)
-        return left or right
-
-class lparen_token(object):
-    "("
-    def nud(self, parser):
-        expr = parser.expression()
-        parser.advance(rparen_token)
-        return expr
-
-class rparen_token(object):
-    ")"
-
-class end_token(object):
-    """always ends parsing"""
-
-### derived literal tokens
-
-class bool_token(literal_token):
-    def __init__(self, value):
-        value = {'true':True, 'false':False}[value]
-        literal_token.__init__(self, value)
-
-class int_token(literal_token):
-    def __init__(self, value):
-        literal_token.__init__(self, int(value))
-
-class string_token(literal_token):
-    def __init__(self, value):
-        literal_token.__init__(self, value[1:-1])
-
-precedence = [(end_token, rparen_token),
-              (or_op_token,),
-              (and_op_token,),
-              (eq_op_token, neq_op_token),
-              (lparen_token,),
-              ]
-for index, rank in enumerate(precedence):
-    for token in rank:
-        token.lbp = index # lbp = lowest left binding power
-
-class ParseError(Exception):
-    """errror parsing conditional expression"""
-
-class ExpressionParser(object):
-    def __init__(self, text, valuemapping, strict=False):
-        """
-        Initialize the parser with input |text|, and |valuemapping| as
-        a dict mapping identifier names to values.
-        """
-        self.text = text
-        self.valuemapping = valuemapping
-        self.strict = strict
-
-    def _tokenize(self):
-        """
-        Lex the input text into tokens and yield them in sequence.
-        """
-        # scanner callbacks
-        def bool_(scanner, t): return bool_token(t)
-        def identifier(scanner, t): return ident_token(t)
-        def integer(scanner, t): return int_token(t)
-        def eq(scanner, t): return eq_op_token()
-        def neq(scanner, t): return neq_op_token()
-        def or_(scanner, t): return or_op_token()
-        def and_(scanner, t): return and_op_token()
-        def lparen(scanner, t): return lparen_token()
-        def rparen(scanner, t): return rparen_token()
-        def string_(scanner, t): return string_token(t)
-        def not_(scanner, t): return not_op_token()
-
-        scanner = re.Scanner([
-            (r"true|false", bool_),
-            (r"[a-zA-Z_]\w*", identifier),
-            (r"[0-9]+", integer),
-            (r'("[^"]*")|(\'[^\']*\')', string_),
-            (r"==", eq),
-            (r"!=", neq),
-            (r"\|\|", or_),
-            (r"!", not_),
-            (r"&&", and_),
-            (r"\(", lparen),
-            (r"\)", rparen),
-            (r"\s+", None), # skip whitespace
-            ])
-        tokens, remainder = scanner.scan(self.text)
-        for t in tokens:
-            yield t
-        yield end_token()
-
-    def value(self, ident):
-        """
-        Look up the value of |ident| in the value mapping passed in the
-        constructor.
-        """
-        if self.strict:
-            return self.valuemapping[ident]
-        else:
-            return self.valuemapping.get(ident, None)
-
-    def advance(self, expected):
-        """
-        Assert that the next token is an instance of |expected|, and advance
-        to the next token.
-        """
-        if not isinstance(self.token, expected):
-            raise Exception, "Unexpected token!"
-        self.token = self.iter.next()
-
-    def expression(self, rbp=0):
-        """
-        Parse and return the value of an expression until a token with
-        right binding power greater than rbp is encountered.
-        """
-        t = self.token
-        self.token = self.iter.next()
-        left = t.nud(self)
-        while rbp < self.token.lbp:
-            t = self.token
-            self.token = self.iter.next()
-            left = t.led(self, left)
-        return left
-
-    def parse(self):
-        """
-        Parse and return the value of the expression in the text
-        passed to the constructor. Raises a ParseError if the expression
-        could not be parsed.
-        """
-        try:
-            self.iter = self._tokenize()
-            self.token = self.iter.next()
-            return self.expression()
-        except:
-            raise ParseError("could not parse: %s; variables: %s" % (self.text, self.valuemapping))
-
-    __call__ = parse
-
-def parse(text, **values):
-    """
-    Parse and evaluate a boolean expression in |text|. Use |values| to look
-    up the value of identifiers referenced in the expression. Returns the final
-    value of the expression. A ParseError will be raised if parsing fails.
-    """
-    return ExpressionParser(text, values).parse()
-
-def normalize_path(path):
-    """normalize a relative path"""
-    if sys.platform.startswith('win'):
-        return path.replace('/', os.path.sep)
-    return path
-
-def denormalize_path(path):
-    """denormalize a relative path"""
-    if sys.platform.startswith('win'):
-        return path.replace(os.path.sep, '/')
-    return path
-
-
-def read_ini(fp, variables=None, default='DEFAULT',
-             comments=';#', separators=('=', ':'),
-             strict=True):
-    """
-    read an .ini file and return a list of [(section, values)]
-    - fp : file pointer or path to read
-    - variables : default set of variables
-    - default : name of the section for the default section
-    - comments : characters that if they start a line denote a comment
-    - separators : strings that denote key, value separation in order
-    - strict : whether to be strict about parsing
-    """
-
-    if variables is None:
-        variables = {}
-
-    if isinstance(fp, basestring):
-        fp = file(fp)
-
-    sections = []
-    key = value = None
-    section_names = set([])
-
-    # read the lines
-    for line in fp.readlines():
-
-        stripped = line.strip()
-
-        # ignore blank lines
-        if not stripped:
-            # reset key and value to avoid continuation lines
-            key = value = None
-            continue
-
-        # ignore comment lines
-        if stripped[0] in comments:
-            continue
-
-        # check for a new section
-        if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']':
-            section = stripped[1:-1].strip()
-            key = value = None
-
-            # deal with DEFAULT section
-            if section.lower() == default.lower():
-                if strict:
-                    assert default not in section_names
-                section_names.add(default)
-                current_section = variables
-                continue
-
-            if strict:
-                # make sure this section doesn't already exist
-                assert section not in section_names
-
-            section_names.add(section)
-            current_section = {}
-            sections.append((section, current_section))
-            continue
-
-        # if there aren't any sections yet, something bad happen
-        if not section_names:
-            raise Exception('No sections found')
-
-        # (key, value) pair
-        for separator in separators:
-            if separator in stripped:
-                key, value = stripped.split(separator, 1)
-                key = key.strip()
-                value = value.strip()
-
-                if strict:
-                    # make sure this key isn't already in the section or empty
-                    assert key
-                    if current_section is not variables:
-                        assert key not in current_section
-
-                current_section[key] = value
-                break
-        else:
-            # continuation line ?
-            if line[0].isspace() and key:
-                value = '%s%s%s' % (value, os.linesep, stripped)
-                current_section[key] = value
-            else:
-                # something bad happen!
-                raise Exception("Not sure what you're trying to do")
-
-    # interpret the variables
-    def interpret_variables(global_dict, local_dict):
-        variables = global_dict.copy()
-        variables.update(local_dict)
-        return variables
-
-    sections = [(i, interpret_variables(variables, j)) for i, j in sections]
-    return sections
-
-
-### objects for parsing manifests
-
-class ManifestParser(object):
-    """read .ini manifests"""
-
-    ### methods for reading manifests
-
-    def __init__(self, manifests=(), defaults=None, strict=True):
-        self._defaults = defaults or {}
-        self.tests = []
-        self.strict = strict
-        self.rootdir = None
-        self.relativeRoot = None
-        if manifests:
-            self.read(*manifests)
-
-    def getRelativeRoot(self, root):
-        return root
-
-    def read(self, *filenames, **defaults):
-
-        # ensure all files exist
-        missing = [ filename for filename in filenames
-                    if not os.path.exists(filename) ]
-        if missing:
-            raise IOError('Missing files: %s' % ', '.join(missing))
-
-        # process each file
-        for filename in filenames:
-
-            # set the per file defaults
-            defaults = defaults.copy() or self._defaults.copy()
-            here = os.path.dirname(os.path.abspath(filename))
-            defaults['here'] = here
-
-            if self.rootdir is None:
-                # set the root directory
-                # == the directory of the first manifest given
-                self.rootdir = here
-
-            # read the configuration
-            sections = read_ini(fp=filename, variables=defaults, strict=self.strict)
-
-            # get the tests
-            for section, data in sections:
-
-                # a file to include
-                # TODO: keep track of included file structure:
-                # self.manifests = {'manifest.ini': 'relative/path.ini'}
-                if section.startswith('include:'):
-                    include_file = section.split('include:', 1)[-1]
-                    include_file = normalize_path(include_file)
-                    if not os.path.isabs(include_file):
-                        include_file = os.path.join(self.getRelativeRoot(here), include_file)
-                    if not os.path.exists(include_file):
-                        if self.strict:
-                            raise IOError("File '%s' does not exist" % include_file)
-                        else:
-                            continue
-                    include_defaults = data.copy()
-                    self.read(include_file, **include_defaults)
-                    continue
-
-                # otherwise an item
-                test = data
-                test['name'] = section
-                test['manifest'] = os.path.abspath(filename)
-
-                # determine the path
-                path = test.get('path', section)
-                if '://' not in path: # don't futz with URLs
-                    path = normalize_path(path)
-                    if not os.path.isabs(path):
-                        path = os.path.join(here, path)
-                test['path'] = path
-
-                # append the item
-                self.tests.append(test)
-
-    ### methods for querying manifests
-
-    def query(self, *checks, **kw):
-        """
-        general query function for tests
-        - checks : callable conditions to test if the test fulfills the query
-        """
-        tests = kw.get('tests', None)
-        if tests is None:
-            tests = self.tests
-        retval = []
-        for test in tests:
-            for check in checks:
-                if not check(test):
-                    break
-            else:
-                retval.append(test)
-        return retval
-
-    def get(self, _key=None, inverse=False, tags=None, tests=None, **kwargs):
-        # TODO: pass a dict instead of kwargs since you might hav
-        # e.g. 'inverse' as a key in the dict
-
-        # TODO: tags should just be part of kwargs with None values
-        # (None == any is kinda weird, but probably still better)
-
-        # fix up tags
-        if tags:
-            tags = set(tags)
-        else:
-            tags = set()
-
-        # make some check functions
-        if inverse:
-            has_tags = lambda test: not tags.intersection(test.keys())
-            def dict_query(test):
-                for key, value in kwargs.items():
-                    if test.get(key) == value:
-                        return False
-                return True
-        else:
-            has_tags = lambda test: tags.issubset(test.keys())
-            def dict_query(test):
-                for key, value in kwargs.items():
-                    if test.get(key) != value:
-                        return False
-                return True
-
-        # query the tests
-        tests = self.query(has_tags, dict_query, tests=tests)
-
-        # if a key is given, return only a list of that key
-        # useful for keys like 'name' or 'path'
-        if _key:
-            return [test[_key] for test in tests]
-
-        # return the tests
-        return tests
-
-    def missing(self, tests=None):
-        """return list of tests that do not exist on the filesystem"""
-        if tests is None:
-            tests = self.tests
-        return [test for test in tests
-                if not os.path.exists(test['path'])]
-
-    def manifests(self, tests=None):
-        """
-        return manifests in order in which they appear in the tests
-        """
-        if tests is None:
-            tests = self.tests
-        manifests = []
-        for test in tests:
-            manifest = test.get('manifest')
-            if not manifest:
-                continue
-            if manifest not in manifests:
-                manifests.append(manifest)
-        return manifests
-
-    ### methods for outputting from manifests
-
-    def write(self, fp=sys.stdout, rootdir=None,
-              global_tags=None, global_kwargs=None,
-              local_tags=None, local_kwargs=None):
-        """
-        write a manifest given a query
-        global and local options will be munged to do the query
-        globals will be written to the top of the file
-        locals (if given) will be written per test
-        """
-
-        # root directory
-        if rootdir is None:
-            rootdir = self.rootdir
-
-        # sanitize input
-        global_tags = global_tags or set()
-        local_tags = local_tags or set()
-        global_kwargs = global_kwargs or {}
-        local_kwargs = local_kwargs or {}
-
-        # create the query
-        tags = set([])
-        tags.update(global_tags)
-        tags.update(local_tags)
-        kwargs = {}
-        kwargs.update(global_kwargs)
-        kwargs.update(local_kwargs)
-
-        # get matching tests
-        tests = self.get(tags=tags, **kwargs)
-
-        # print the .ini manifest
-        if global_tags or global_kwargs:
-            print >> fp, '[DEFAULT]'
-            for tag in global_tags:
-                print >> fp, '%s =' % tag
-            for key, value in global_kwargs.items():
-                print >> fp, '%s = %s' % (key, value)
-            print >> fp
-
-        for test in tests:
-            test = test.copy() # don't overwrite
-
-            path = test['name']
-            if not os.path.isabs(path):
-                path = test['path']
-                if self.rootdir:
-                    path = relpath(test['path'], self.rootdir)
-                path = denormalize_path(path)
-            print >> fp, '[%s]' % path
-
-            # reserved keywords:
-            reserved = ['path', 'name', 'here', 'manifest']
-            for key in sorted(test.keys()):
-                if key in reserved:
-                    continue
-                if key in global_kwargs:
-                    continue
-                if key in global_tags and not test[key]:
-                    continue
-                print >> fp, '%s = %s' % (key, test[key])
-            print >> fp
-
-    def copy(self, directory, rootdir=None, *tags, **kwargs):
-        """
-        copy the manifests and associated tests
-        - directory : directory to copy to
-        - rootdir : root directory to copy to (if not given from manifests)
-        - tags : keywords the tests must have
-        - kwargs : key, values the tests must match
-        """
-        # XXX note that copy does *not* filter the tests out of the
-        # resulting manifest; it just stupidly copies them over.
-        # ideally, it would reread the manifests and filter out the
-        # tests that don't match *tags and **kwargs
-
-        # destination
-        if not os.path.exists(directory):
-            os.path.makedirs(directory)
-        else:
-            # sanity check
-            assert os.path.isdir(directory)
-
-        # tests to copy
-        tests = self.get(tags=tags, **kwargs)
-        if not tests:
-            return # nothing to do!
-
-        # root directory
-        if rootdir is None:
-            rootdir = self.rootdir
-
-        # copy the manifests + tests
-        manifests = [relpath(manifest, rootdir) for manifest in self.manifests()]
-        for manifest in manifests:
-            destination = os.path.join(directory, manifest)
-            dirname = os.path.dirname(destination)
-            if not os.path.exists(dirname):
-                os.makedirs(dirname)
-            else:
-                # sanity check
-                assert os.path.isdir(dirname)
-            shutil.copy(os.path.join(rootdir, manifest), destination)
-        for test in tests:
-            if os.path.isabs(test['name']):
-                continue
-            source = test['path']
-            if not os.path.exists(source):
-                print >> sys.stderr, "Missing test: '%s' does not exist!" % source
-                continue
-                # TODO: should err on strict
-            destination = os.path.join(directory, relpath(test['path'], rootdir))
-            shutil.copy(source, destination)
-            # TODO: ensure that all of the tests are below the from_dir
-
-    def update(self, from_dir, rootdir=None, *tags, **kwargs):
-        """
-        update the tests as listed in a manifest from a directory
-        - from_dir : directory where the tests live
-        - rootdir : root directory to copy to (if not given from manifests)
-        - tags : keys the tests must have
-        - kwargs : key, values the tests must match
-        """
-
-        # get the tests
-        tests = self.get(tags=tags, **kwargs)
-
-        # get the root directory
-        if not rootdir:
-            rootdir = self.rootdir
-
-        # copy them!
-        for test in tests:
-            if not os.path.isabs(test['name']):
-                _relpath = relpath(test['path'], rootdir)
-                source = os.path.join(from_dir, _relpath)
-                if not os.path.exists(source):
-                    # TODO err on strict
-                    print >> sys.stderr, "Missing test: '%s'; skipping" % test['name']
-                    continue
-                destination = os.path.join(rootdir, _relpath)
-                shutil.copy(source, destination)
-
-
-class TestManifest(ManifestParser):
-    """
-    apply logic to manifests;  this is your integration layer :)
-    specific harnesses may subclass from this if they need more logic
-    """
-
-    def filter(self, values, tests):
-        """
-        filter on a specific list tag, e.g.:
-        run-if.os = win linux
-        skip-if.os = mac
-        """
-
-        # tags:
-        run_tag = 'run-if'
-        skip_tag = 'skip-if'
-        fail_tag = 'fail-if'
-
-        # loop over test
-        for test in tests:
-            reason = None # reason to disable
-
-            # tagged-values to run
-            if run_tag in test:
-                condition = test[run_tag]
-                if not parse(condition, **values):
-                    reason = '%s: %s' % (run_tag, condition)
-
-            # tagged-values to skip
-            if skip_tag in test:
-                condition = test[skip_tag]
-                if parse(condition, **values):
-                    reason = '%s: %s' % (skip_tag, condition)
-
-            # mark test as disabled if there's a reason
-            if reason:
-                test.setdefault('disabled', reason)
-
-            # mark test as a fail if so indicated
-            if fail_tag in test:
-                condition = test[fail_tag]
-                if parse(condition, **values):
-                    test['expected'] = 'fail'
-
-    def active_tests(self, exists=True, disabled=True, **values):
-        """
-        - exists : return only existing tests
-        - disabled : whether to return disabled tests
-        - tags : keys and values to filter on (e.g. `os = linux mac`)
-        """
-
-        tests = [i.copy() for i in self.tests] # shallow copy
-
-        # mark all tests as passing unless indicated otherwise
-        for test in tests:
-            test['expected'] = test.get('expected', 'pass')
-
-        # ignore tests that do not exist
-        if exists:
-            tests = [test for test in tests if os.path.exists(test['path'])]
-
-        # filter by tags
-        self.filter(values, tests)
-
-        # ignore disabled tests if specified
-        if not disabled:
-            tests = [test for test in tests
-                     if not 'disabled' in test]
-
-        # return active tests
-        return tests
-
-    def test_paths(self):
-        return [test['path'] for test in self.active_tests()]
-
-
-### utility function(s); probably belongs elsewhere
-
-def convert(directories, pattern=None, ignore=(), write=None):
-    """
-    convert directories to a simple manifest
-    """
-
-    retval = []
-    include = []
-    for directory in directories:
-        for dirpath, dirnames, filenames in os.walk(directory):
-
-            # filter out directory names
-            dirnames = [ i for i in dirnames if i not in ignore ]
-            dirnames.sort()
-
-            # reference only the subdirectory
-            _dirpath = dirpath
-            dirpath = dirpath.split(directory, 1)[-1].strip(os.path.sep)
-
-            if dirpath.split(os.path.sep)[0] in ignore:
-                continue
-
-            # filter by glob
-            if pattern:
-                filenames = [filename for filename in filenames
-                             if fnmatch(filename, pattern)]
-
-            filenames.sort()
-
-            # write a manifest for each directory
-            if write and (dirnames or filenames):
-                manifest = file(os.path.join(_dirpath, write), 'w')
-                for dirname in dirnames:
-                    print >> manifest, '[include:%s]' % os.path.join(dirname, write)
-                for filename in filenames:
-                    print >> manifest, '[%s]' % filename
-                manifest.close()
-
-            # add to the list
-            retval.extend([denormalize_path(os.path.join(dirpath, filename))
-                           for filename in filenames])
-
-    if write:
-        return # the manifests have already been written!
-
-    retval.sort()
-    retval = ['[%s]' % filename for filename in retval]
-    return '\n'.join(retval)
-
-### command line attributes
-
-class ParserError(Exception):
-  """error for exceptions while parsing the command line"""
-
-def parse_args(_args):
-    """
-    parse and return:
-    --keys=value (or --key value)
-    -tags
-    args
-    """
-
-    # return values
-    _dict = {}
-    tags = []
-    args = []
-
-    # parse the arguments
-    key = None
-    for arg in _args:
-        if arg.startswith('---'):
-            raise ParserError("arguments should start with '-' or '--' only")
-        elif arg.startswith('--'):
-            if key:
-                raise ParserError("Key %s still open" % key)
-            key = arg[2:]
-            if '=' in key:
-                key, value = key.split('=', 1)
-                _dict[key] = value
-                key = None
-                continue
-        elif arg.startswith('-'):
-            if key:
-                raise ParserError("Key %s still open" % key)
-            tags.append(arg[1:])
-            continue
-        else:
-            if key:
-                _dict[key] = arg
-                continue
-            args.append(arg)
-
-    # return values
-    return (_dict, tags, args)
-
-
-### classes for subcommands
-
-class CLICommand(object):
-    usage = '%prog [options] command'
-    def __init__(self, parser):
-      self._parser = parser # master parser
-    def parser(self):
-      return OptionParser(usage=self.usage, description=self.__doc__,
-                          add_help_option=False)
-
-class Copy(CLICommand):
-    usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
-    def __call__(self, options, args):
-      # parse the arguments
-      try:
-        kwargs, tags, args = parse_args(args)
-      except ParserError, e:
-        self._parser.error(e.message)
-
-      # make sure we have some manifests, otherwise it will
-      # be quite boring
-      if not len(args) == 2:
-        HelpCLI(self._parser)(options, ['copy'])
-        return
-
-      # read the manifests
-      # TODO: should probably ensure these exist here
-      manifests = ManifestParser()
-      manifests.read(args[0])
-
-      # print the resultant query
-      manifests.copy(args[1], None, *tags, **kwargs)
-
-
-class CreateCLI(CLICommand):
-    """
-    create a manifest from a list of directories
-    """
-    usage = '%prog [options] create directory <directory> <...>'
-
-    def parser(self):
-        parser = CLICommand.parser(self)
-        parser.add_option('-p', '--pattern', dest='pattern',
-                          help="glob pattern for files")
-        parser.add_option('-i', '--ignore', dest='ignore',
-                          default=[], action='append',
-                          help='directories to ignore')
-        parser.add_option('-w', '--in-place', dest='in_place',
-                          help='Write .ini files in place; filename to write to')
-        return parser
-
-    def __call__(self, _options, args):
-        parser = self.parser()
-        options, args = parser.parse_args(args)
-
-        # need some directories
-        if not len(args):
-            parser.print_usage()
-            return
-
-        # add the directories to the manifest
-        for arg in args:
-            assert os.path.exists(arg)
-            assert os.path.isdir(arg)
-            manifest = convert(args, pattern=options.pattern, ignore=options.ignore,
-                               write=options.in_place)
-        if manifest:
-            print manifest
-
-
-class WriteCLI(CLICommand):
-    """
-    write a manifest based on a query
-    """
-    usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...'
-    def __call__(self, options, args):
-
-        # parse the arguments
-        try:
-            kwargs, tags, args = parse_args(args)
-        except ParserError, e:
-            self._parser.error(e.message)
-
-        # make sure we have some manifests, otherwise it will
-        # be quite boring
-        if not args:
-            HelpCLI(self._parser)(options, ['write'])
-            return
-
-        # read the manifests
-        # TODO: should probably ensure these exist here
-        manifests = ManifestParser()
-        manifests.read(*args)
-
-        # print the resultant query
-        manifests.write(global_tags=tags, global_kwargs=kwargs)
-
-
-class HelpCLI(CLICommand):
-    """
-    get help on a command
-    """
-    usage = '%prog [options] help [command]'
-
-    def __call__(self, options, args):
-        if len(args) == 1 and args[0] in commands:
-            commands[args[0]](self._parser).parser().print_help()
-        else:
-            self._parser.print_help()
-            print '\nCommands:'
-            for command in sorted(commands):
-                print '  %s : %s' % (command, commands[command].__doc__.strip())
-
-class SetupCLI(CLICommand):
-    """
-    setup using setuptools
-    """
-    # use setup.py from the repo when you want to distribute to python!
-    # otherwise setuptools will complain that it can't find setup.py
-    # and result in a useless package
-
-    usage = '%prog [options] setup [setuptools options]'
-
-    def __call__(self, options, args):
-        sys.argv = [sys.argv[0]] + args
-        assert setup is not None, "You must have setuptools installed to use SetupCLI"
-        here = os.path.dirname(os.path.abspath(__file__))
-        try:
-            filename = os.path.join(here, 'README.txt')
-            description = file(filename).read()
-        except:
-            description = ''
-        os.chdir(here)
-
-        setup(name='ManifestDestiny',
-              version=version,
-              description="Universal manifests for Mozilla test harnesses",
-              long_description=description,
-              classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
-              keywords='mozilla manifests',
-              author='Jeff Hammel',
-              author_email='jhammel@mozilla.com',
-              url='https://wiki.mozilla.org/Auto-tools/Projects/ManifestDestiny',
-              license='MPL',
-              zip_safe=False,
-              py_modules=['manifestparser'],
-              install_requires=[
-                  # -*- Extra requirements: -*-
-                  ],
-              entry_points="""
-              [console_scripts]
-              manifestparser = manifestparser:main
-              """,
-              )
-
-
-class UpdateCLI(CLICommand):
-    """
-    update the tests as listed in a manifest from a directory
-    """
-    usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
-
-    def __call__(self, options, args):
-        # parse the arguments
-        try:
-            kwargs, tags, args = parse_args(args)
-        except ParserError, e:
-            self._parser.error(e.message)
-
-        # make sure we have some manifests, otherwise it will
-        # be quite boring
-        if not len(args) == 2:
-            HelpCLI(self._parser)(options, ['update'])
-            return
-
-        # read the manifests
-        # TODO: should probably ensure these exist here
-        manifests = ManifestParser()
-        manifests.read(args[0])
-
-        # print the resultant query
-        manifests.update(args[1], None, *tags, **kwargs)
-
-
-# command -> class mapping
-commands = { 'create': CreateCLI,
-             'help': HelpCLI,
-             'update': UpdateCLI,
-             'write': WriteCLI }
-if setup is not None:
-    commands['setup'] = SetupCLI
-
-def main(args=sys.argv[1:]):
-    """console_script entry point"""
-
-    # set up an option parser
-    usage = '%prog [options] [command] ...'
-    description = __doc__
-    parser = OptionParser(usage=usage, description=description)
-    parser.add_option('-s', '--strict', dest='strict',
-                      action='store_true', default=False,
-                      help='adhere strictly to errors')
-    parser.disable_interspersed_args()
-
-    options, args = parser.parse_args(args)
-
-    if not args:
-        HelpCLI(parser)(options, args)
-        parser.exit()
-
-    # get the command
-    command = args[0]
-    if command not in commands:
-        parser.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands.keys())), command))
-
-    handler = commands[command](parser)
-    handler(options, args[1:])
-
-if __name__ == '__main__':
-    main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/manifestparser/__init__.py
@@ -0,0 +1,38 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is manifestdestiny.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#     Jeff Hammel <jhammel@mozilla.com>     (Original author)
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+from manifestparser import *
new file mode 100755
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/manifestparser/manifestparser.py
@@ -0,0 +1,1069 @@
+#!/usr/bin/env python
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is manifestdestiny.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#     Jeff Hammel <jhammel@mozilla.com>     (Original author)
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+"""
+Mozilla universal manifest parser
+"""
+
+# this file lives at
+# http://hg.mozilla.org/automation/ManifestDestiny/raw-file/tip/manifestparser.py
+
+__all__ = ['read_ini', # .ini reader
+           'ManifestParser', 'TestManifest', 'convert', # manifest handling
+           'parse', 'ParseError', 'ExpressionParser'] # conditional expression parser
+
+import os
+import re
+import shutil
+import sys
+from fnmatch import fnmatch
+from optparse import OptionParser
+
+version = '0.5.4' # package version
+try:
+    from setuptools import setup
+except:
+    setup = None
+
+# 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 start
+        return os.path.join(*rel_list)
+
+# 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
+#
+# Rough grammar:
+# expr := literal
+#       | '(' expr ')'
+#       | expr '&&' expr
+#       | expr '||' expr
+#       | expr '==' expr
+#       | expr '!=' expr
+# literal := BOOL
+#          | INT
+#          | STRING
+#          | IDENT
+# BOOL   := true|false
+# INT    := [0-9]+
+# STRING := "[^"]*"
+# IDENT  := [A-Za-z_]\w*
+
+# Identifiers take their values from a mapping dictionary passed as the second
+# argument.
+
+# Glossary (see above URL for details):
+# - nud: null denotation
+# - led: left detonation
+# - lbp: left binding power
+# - rbp: right binding power
+
+class ident_token(object):
+    def __init__(self, value):
+        self.value = value
+    def nud(self, parser):
+        # identifiers take their value from the value mappings passed
+        # to the parser
+        return parser.value(self.value)
+
+class literal_token(object):
+    def __init__(self, value):
+        self.value = value
+    def nud(self, parser):
+        return self.value
+
+class eq_op_token(object):
+    "=="
+    def led(self, parser, left):
+        return left == parser.expression(self.lbp)
+
+class neq_op_token(object):
+    "!="
+    def led(self, parser, left):
+        return left != parser.expression(self.lbp)
+
+class not_op_token(object):
+    "!"
+    def nud(self, parser):
+        return not parser.expression()
+
+class and_op_token(object):
+    "&&"
+    def led(self, parser, left):
+        right = parser.expression(self.lbp)
+        return left and right
+
+class or_op_token(object):
+    "||"
+    def led(self, parser, left):
+        right = parser.expression(self.lbp)
+        return left or right
+
+class lparen_token(object):
+    "("
+    def nud(self, parser):
+        expr = parser.expression()
+        parser.advance(rparen_token)
+        return expr
+
+class rparen_token(object):
+    ")"
+
+class end_token(object):
+    """always ends parsing"""
+
+### derived literal tokens
+
+class bool_token(literal_token):
+    def __init__(self, value):
+        value = {'true':True, 'false':False}[value]
+        literal_token.__init__(self, value)
+
+class int_token(literal_token):
+    def __init__(self, value):
+        literal_token.__init__(self, int(value))
+
+class string_token(literal_token):
+    def __init__(self, value):
+        literal_token.__init__(self, value[1:-1])
+
+precedence = [(end_token, rparen_token),
+              (or_op_token,),
+              (and_op_token,),
+              (eq_op_token, neq_op_token),
+              (lparen_token,),
+              ]
+for index, rank in enumerate(precedence):
+    for token in rank:
+        token.lbp = index # lbp = lowest left binding power
+
+class ParseError(Exception):
+    """errror parsing conditional expression"""
+
+class ExpressionParser(object):
+    def __init__(self, text, valuemapping, strict=False):
+        """
+        Initialize the parser with input |text|, and |valuemapping| as
+        a dict mapping identifier names to values.
+        """
+        self.text = text
+        self.valuemapping = valuemapping
+        self.strict = strict
+
+    def _tokenize(self):
+        """
+        Lex the input text into tokens and yield them in sequence.
+        """
+        # scanner callbacks
+        def bool_(scanner, t): return bool_token(t)
+        def identifier(scanner, t): return ident_token(t)
+        def integer(scanner, t): return int_token(t)
+        def eq(scanner, t): return eq_op_token()
+        def neq(scanner, t): return neq_op_token()
+        def or_(scanner, t): return or_op_token()
+        def and_(scanner, t): return and_op_token()
+        def lparen(scanner, t): return lparen_token()
+        def rparen(scanner, t): return rparen_token()
+        def string_(scanner, t): return string_token(t)
+        def not_(scanner, t): return not_op_token()
+
+        scanner = re.Scanner([
+            (r"true|false", bool_),
+            (r"[a-zA-Z_]\w*", identifier),
+            (r"[0-9]+", integer),
+            (r'("[^"]*")|(\'[^\']*\')', string_),
+            (r"==", eq),
+            (r"!=", neq),
+            (r"\|\|", or_),
+            (r"!", not_),
+            (r"&&", and_),
+            (r"\(", lparen),
+            (r"\)", rparen),
+            (r"\s+", None), # skip whitespace
+            ])
+        tokens, remainder = scanner.scan(self.text)
+        for t in tokens:
+            yield t
+        yield end_token()
+
+    def value(self, ident):
+        """
+        Look up the value of |ident| in the value mapping passed in the
+        constructor.
+        """
+        if self.strict:
+            return self.valuemapping[ident]
+        else:
+            return self.valuemapping.get(ident, None)
+
+    def advance(self, expected):
+        """
+        Assert that the next token is an instance of |expected|, and advance
+        to the next token.
+        """
+        if not isinstance(self.token, expected):
+            raise Exception, "Unexpected token!"
+        self.token = self.iter.next()
+
+    def expression(self, rbp=0):
+        """
+        Parse and return the value of an expression until a token with
+        right binding power greater than rbp is encountered.
+        """
+        t = self.token
+        self.token = self.iter.next()
+        left = t.nud(self)
+        while rbp < self.token.lbp:
+            t = self.token
+            self.token = self.iter.next()
+            left = t.led(self, left)
+        return left
+
+    def parse(self):
+        """
+        Parse and return the value of the expression in the text
+        passed to the constructor. Raises a ParseError if the expression
+        could not be parsed.
+        """
+        try:
+            self.iter = self._tokenize()
+            self.token = self.iter.next()
+            return self.expression()
+        except:
+            raise ParseError("could not parse: %s; variables: %s" % (self.text, self.valuemapping))
+
+    __call__ = parse
+
+def parse(text, **values):
+    """
+    Parse and evaluate a boolean expression in |text|. Use |values| to look
+    up the value of identifiers referenced in the expression. Returns the final
+    value of the expression. A ParseError will be raised if parsing fails.
+    """
+    return ExpressionParser(text, values).parse()
+
+def normalize_path(path):
+    """normalize a relative path"""
+    if sys.platform.startswith('win'):
+        return path.replace('/', os.path.sep)
+    return path
+
+def denormalize_path(path):
+    """denormalize a relative path"""
+    if sys.platform.startswith('win'):
+        return path.replace(os.path.sep, '/')
+    return path
+
+
+def read_ini(fp, variables=None, default='DEFAULT',
+             comments=';#', separators=('=', ':'),
+             strict=True):
+    """
+    read an .ini file and return a list of [(section, values)]
+    - fp : file pointer or path to read
+    - variables : default set of variables
+    - default : name of the section for the default section
+    - comments : characters that if they start a line denote a comment
+    - separators : strings that denote key, value separation in order
+    - strict : whether to be strict about parsing
+    """
+
+    if variables is None:
+        variables = {}
+
+    if isinstance(fp, basestring):
+        fp = file(fp)
+
+    sections = []
+    key = value = None
+    section_names = set([])
+
+    # read the lines
+    for line in fp.readlines():
+
+        stripped = line.strip()
+
+        # ignore blank lines
+        if not stripped:
+            # reset key and value to avoid continuation lines
+            key = value = None
+            continue
+
+        # ignore comment lines
+        if stripped[0] in comments:
+            continue
+
+        # check for a new section
+        if len(stripped) > 2 and stripped[0] == '[' and stripped[-1] == ']':
+            section = stripped[1:-1].strip()
+            key = value = None
+
+            # deal with DEFAULT section
+            if section.lower() == default.lower():
+                if strict:
+                    assert default not in section_names
+                section_names.add(default)
+                current_section = variables
+                continue
+
+            if strict:
+                # make sure this section doesn't already exist
+                assert section not in section_names, "Section '%s' already found in '%s'" % (section, section_names)
+
+            section_names.add(section)
+            current_section = {}
+            sections.append((section, current_section))
+            continue
+
+        # if there aren't any sections yet, something bad happen
+        if not section_names:
+            raise Exception('No sections found')
+
+        # (key, value) pair
+        for separator in separators:
+            if separator in stripped:
+                key, value = stripped.split(separator, 1)
+                key = key.strip()
+                value = value.strip()
+
+                if strict:
+                    # make sure this key isn't already in the section or empty
+                    assert key
+                    if current_section is not variables:
+                        assert key not in current_section
+
+                current_section[key] = value
+                break
+        else:
+            # continuation line ?
+            if line[0].isspace() and key:
+                value = '%s%s%s' % (value, os.linesep, stripped)
+                current_section[key] = value
+            else:
+                # something bad happen!
+                raise Exception("Not sure what you're trying to do")
+
+    # interpret the variables
+    def interpret_variables(global_dict, local_dict):
+        variables = global_dict.copy()
+        variables.update(local_dict)
+        return variables
+
+    sections = [(i, interpret_variables(variables, j)) for i, j in sections]
+    return sections
+
+
+### objects for parsing manifests
+
+class ManifestParser(object):
+    """read .ini manifests"""
+
+    ### methods for reading manifests
+
+    def __init__(self, manifests=(), defaults=None, strict=True):
+        self._defaults = defaults or {}
+        self.tests = []
+        self.strict = strict
+        self.rootdir = None
+        self.relativeRoot = None
+        if manifests:
+            self.read(*manifests)
+
+    def getRelativeRoot(self, root):
+        return root
+
+    def read(self, *filenames, **defaults):
+
+        # ensure all files exist
+        missing = [ filename for filename in filenames
+                    if not os.path.exists(filename) ]
+        if missing:
+            raise IOError('Missing files: %s' % ', '.join(missing))
+
+        # process each file
+        for filename in filenames:
+
+            # set the per file defaults
+            defaults = defaults.copy() or self._defaults.copy()
+            here = os.path.dirname(os.path.abspath(filename))
+            defaults['here'] = here
+
+            if self.rootdir is None:
+                # set the root directory
+                # == the directory of the first manifest given
+                self.rootdir = here
+
+            # read the configuration
+            sections = read_ini(fp=filename, variables=defaults, strict=self.strict)
+
+            # get the tests
+            for section, data in sections:
+
+                # a file to include
+                # TODO: keep track of included file structure:
+                # self.manifests = {'manifest.ini': 'relative/path.ini'}
+                if section.startswith('include:'):
+                    include_file = section.split('include:', 1)[-1]
+                    include_file = normalize_path(include_file)
+                    if not os.path.isabs(include_file):
+                        include_file = os.path.join(self.getRelativeRoot(here), include_file)
+                    if not os.path.exists(include_file):
+                        if self.strict:
+                            raise IOError("File '%s' does not exist" % include_file)
+                        else:
+                            continue
+                    include_defaults = data.copy()
+                    self.read(include_file, **include_defaults)
+                    continue
+
+                # otherwise an item
+                test = data
+                test['name'] = section
+                test['manifest'] = os.path.abspath(filename)
+
+                # determine the path
+                path = test.get('path', section)
+                if '://' not in path: # don't futz with URLs
+                    path = normalize_path(path)
+                    if not os.path.isabs(path):
+                        path = os.path.join(here, path)
+                test['path'] = path
+
+                # append the item
+                self.tests.append(test)
+
+    ### methods for querying manifests
+
+    def query(self, *checks, **kw):
+        """
+        general query function for tests
+        - checks : callable conditions to test if the test fulfills the query
+        """
+        tests = kw.get('tests', None)
+        if tests is None:
+            tests = self.tests
+        retval = []
+        for test in tests:
+            for check in checks:
+                if not check(test):
+                    break
+            else:
+                retval.append(test)
+        return retval
+
+    def get(self, _key=None, inverse=False, tags=None, tests=None, **kwargs):
+        # TODO: pass a dict instead of kwargs since you might hav
+        # e.g. 'inverse' as a key in the dict
+
+        # TODO: tags should just be part of kwargs with None values
+        # (None == any is kinda weird, but probably still better)
+
+        # fix up tags
+        if tags:
+            tags = set(tags)
+        else:
+            tags = set()
+
+        # make some check functions
+        if inverse:
+            has_tags = lambda test: not tags.intersection(test.keys())
+            def dict_query(test):
+                for key, value in kwargs.items():
+                    if test.get(key) == value:
+                        return False
+                return True
+        else:
+            has_tags = lambda test: tags.issubset(test.keys())
+            def dict_query(test):
+                for key, value in kwargs.items():
+                    if test.get(key) != value:
+                        return False
+                return True
+
+        # query the tests
+        tests = self.query(has_tags, dict_query, tests=tests)
+
+        # if a key is given, return only a list of that key
+        # useful for keys like 'name' or 'path'
+        if _key:
+            return [test[_key] for test in tests]
+
+        # return the tests
+        return tests
+
+    def missing(self, tests=None):
+        """return list of tests that do not exist on the filesystem"""
+        if tests is None:
+            tests = self.tests
+        return [test for test in tests
+                if not os.path.exists(test['path'])]
+
+    def manifests(self, tests=None):
+        """
+        return manifests in order in which they appear in the tests
+        """
+        if tests is None:
+            tests = self.tests
+        manifests = []
+        for test in tests:
+            manifest = test.get('manifest')
+            if not manifest:
+                continue
+            if manifest not in manifests:
+                manifests.append(manifest)
+        return manifests
+
+    ### methods for outputting from manifests
+
+    def write(self, fp=sys.stdout, rootdir=None,
+              global_tags=None, global_kwargs=None,
+              local_tags=None, local_kwargs=None):
+        """
+        write a manifest given a query
+        global and local options will be munged to do the query
+        globals will be written to the top of the file
+        locals (if given) will be written per test
+        """
+
+        # root directory
+        if rootdir is None:
+            rootdir = self.rootdir
+
+        # sanitize input
+        global_tags = global_tags or set()
+        local_tags = local_tags or set()
+        global_kwargs = global_kwargs or {}
+        local_kwargs = local_kwargs or {}
+
+        # create the query
+        tags = set([])
+        tags.update(global_tags)
+        tags.update(local_tags)
+        kwargs = {}
+        kwargs.update(global_kwargs)
+        kwargs.update(local_kwargs)
+
+        # get matching tests
+        tests = self.get(tags=tags, **kwargs)
+
+        # print the .ini manifest
+        if global_tags or global_kwargs:
+            print >> fp, '[DEFAULT]'
+            for tag in global_tags:
+                print >> fp, '%s =' % tag
+            for key, value in global_kwargs.items():
+                print >> fp, '%s = %s' % (key, value)
+            print >> fp
+
+        for test in tests:
+            test = test.copy() # don't overwrite
+
+            path = test['name']
+            if not os.path.isabs(path):
+                path = test['path']
+                if self.rootdir:
+                    path = relpath(test['path'], self.rootdir)
+                path = denormalize_path(path)
+            print >> fp, '[%s]' % path
+
+            # reserved keywords:
+            reserved = ['path', 'name', 'here', 'manifest']
+            for key in sorted(test.keys()):
+                if key in reserved:
+                    continue
+                if key in global_kwargs:
+                    continue
+                if key in global_tags and not test[key]:
+                    continue
+                print >> fp, '%s = %s' % (key, test[key])
+            print >> fp
+
+    def copy(self, directory, rootdir=None, *tags, **kwargs):
+        """
+        copy the manifests and associated tests
+        - directory : directory to copy to
+        - rootdir : root directory to copy to (if not given from manifests)
+        - tags : keywords the tests must have
+        - kwargs : key, values the tests must match
+        """
+        # XXX note that copy does *not* filter the tests out of the
+        # resulting manifest; it just stupidly copies them over.
+        # ideally, it would reread the manifests and filter out the
+        # tests that don't match *tags and **kwargs
+
+        # destination
+        if not os.path.exists(directory):
+            os.path.makedirs(directory)
+        else:
+            # sanity check
+            assert os.path.isdir(directory)
+
+        # tests to copy
+        tests = self.get(tags=tags, **kwargs)
+        if not tests:
+            return # nothing to do!
+
+        # root directory
+        if rootdir is None:
+            rootdir = self.rootdir
+
+        # copy the manifests + tests
+        manifests = [relpath(manifest, rootdir) for manifest in self.manifests()]
+        for manifest in manifests:
+            destination = os.path.join(directory, manifest)
+            dirname = os.path.dirname(destination)
+            if not os.path.exists(dirname):
+                os.makedirs(dirname)
+            else:
+                # sanity check
+                assert os.path.isdir(dirname)
+            shutil.copy(os.path.join(rootdir, manifest), destination)
+        for test in tests:
+            if os.path.isabs(test['name']):
+                continue
+            source = test['path']
+            if not os.path.exists(source):
+                print >> sys.stderr, "Missing test: '%s' does not exist!" % source
+                continue
+                # TODO: should err on strict
+            destination = os.path.join(directory, relpath(test['path'], rootdir))
+            shutil.copy(source, destination)
+            # TODO: ensure that all of the tests are below the from_dir
+
+    def update(self, from_dir, rootdir=None, *tags, **kwargs):
+        """
+        update the tests as listed in a manifest from a directory
+        - from_dir : directory where the tests live
+        - rootdir : root directory to copy to (if not given from manifests)
+        - tags : keys the tests must have
+        - kwargs : key, values the tests must match
+        """
+
+        # get the tests
+        tests = self.get(tags=tags, **kwargs)
+
+        # get the root directory
+        if not rootdir:
+            rootdir = self.rootdir
+
+        # copy them!
+        for test in tests:
+            if not os.path.isabs(test['name']):
+                _relpath = relpath(test['path'], rootdir)
+                source = os.path.join(from_dir, _relpath)
+                if not os.path.exists(source):
+                    # TODO err on strict
+                    print >> sys.stderr, "Missing test: '%s'; skipping" % test['name']
+                    continue
+                destination = os.path.join(rootdir, _relpath)
+                shutil.copy(source, destination)
+
+
+class TestManifest(ManifestParser):
+    """
+    apply logic to manifests;  this is your integration layer :)
+    specific harnesses may subclass from this if they need more logic
+    """
+
+    def filter(self, values, tests):
+        """
+        filter on a specific list tag, e.g.:
+        run-if.os = win linux
+        skip-if.os = mac
+        """
+
+        # tags:
+        run_tag = 'run-if'
+        skip_tag = 'skip-if'
+        fail_tag = 'fail-if'
+
+        # loop over test
+        for test in tests:
+            reason = None # reason to disable
+
+            # tagged-values to run
+            if run_tag in test:
+                condition = test[run_tag]
+                if not parse(condition, **values):
+                    reason = '%s: %s' % (run_tag, condition)
+
+            # tagged-values to skip
+            if skip_tag in test:
+                condition = test[skip_tag]
+                if parse(condition, **values):
+                    reason = '%s: %s' % (skip_tag, condition)
+
+            # mark test as disabled if there's a reason
+            if reason:
+                test.setdefault('disabled', reason)
+
+            # mark test as a fail if so indicated
+            if fail_tag in test:
+                condition = test[fail_tag]
+                if parse(condition, **values):
+                    test['expected'] = 'fail'
+
+    def active_tests(self, exists=True, disabled=True, **values):
+        """
+        - exists : return only existing tests
+        - disabled : whether to return disabled tests
+        - tags : keys and values to filter on (e.g. `os = linux mac`)
+        """
+
+        tests = [i.copy() for i in self.tests] # shallow copy
+
+        # mark all tests as passing unless indicated otherwise
+        for test in tests:
+            test['expected'] = test.get('expected', 'pass')
+
+        # ignore tests that do not exist
+        if exists:
+            tests = [test for test in tests if os.path.exists(test['path'])]
+
+        # filter by tags
+        self.filter(values, tests)
+
+        # ignore disabled tests if specified
+        if not disabled:
+            tests = [test for test in tests
+                     if not 'disabled' in test]
+
+        # return active tests
+        return tests
+
+    def test_paths(self):
+        return [test['path'] for test in self.active_tests()]
+
+
+### utility function(s); probably belongs elsewhere
+
+def convert(directories, pattern=None, ignore=(), write=None):
+    """
+    convert directories to a simple manifest
+    """
+
+    retval = []
+    include = []
+    for directory in directories:
+        for dirpath, dirnames, filenames in os.walk(directory):
+
+            # filter out directory names
+            dirnames = [ i for i in dirnames if i not in ignore ]
+            dirnames.sort()
+
+            # reference only the subdirectory
+            _dirpath = dirpath
+            dirpath = dirpath.split(directory, 1)[-1].strip(os.path.sep)
+
+            if dirpath.split(os.path.sep)[0] in ignore:
+                continue
+
+            # filter by glob
+            if pattern:
+                filenames = [filename for filename in filenames
+                             if fnmatch(filename, pattern)]
+
+            filenames.sort()
+
+            # write a manifest for each directory
+            if write and (dirnames or filenames):
+                manifest = file(os.path.join(_dirpath, write), 'w')
+                for dirname in dirnames:
+                    print >> manifest, '[include:%s]' % os.path.join(dirname, write)
+                for filename in filenames:
+                    print >> manifest, '[%s]' % filename
+                manifest.close()
+
+            # add to the list
+            retval.extend([denormalize_path(os.path.join(dirpath, filename))
+                           for filename in filenames])
+
+    if write:
+        return # the manifests have already been written!
+
+    retval.sort()
+    retval = ['[%s]' % filename for filename in retval]
+    return '\n'.join(retval)
+
+### command line attributes
+
+class ParserError(Exception):
+  """error for exceptions while parsing the command line"""
+
+def parse_args(_args):
+    """
+    parse and return:
+    --keys=value (or --key value)
+    -tags
+    args
+    """
+
+    # return values
+    _dict = {}
+    tags = []
+    args = []
+
+    # parse the arguments
+    key = None
+    for arg in _args:
+        if arg.startswith('---'):
+            raise ParserError("arguments should start with '-' or '--' only")
+        elif arg.startswith('--'):
+            if key:
+                raise ParserError("Key %s still open" % key)
+            key = arg[2:]
+            if '=' in key:
+                key, value = key.split('=', 1)
+                _dict[key] = value
+                key = None
+                continue
+        elif arg.startswith('-'):
+            if key:
+                raise ParserError("Key %s still open" % key)
+            tags.append(arg[1:])
+            continue
+        else:
+            if key:
+                _dict[key] = arg
+                continue
+            args.append(arg)
+
+    # return values
+    return (_dict, tags, args)
+
+
+### classes for subcommands
+
+class CLICommand(object):
+    usage = '%prog [options] command'
+    def __init__(self, parser):
+      self._parser = parser # master parser
+    def parser(self):
+      return OptionParser(usage=self.usage, description=self.__doc__,
+                          add_help_option=False)
+
+class Copy(CLICommand):
+    usage = '%prog [options] copy manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
+    def __call__(self, options, args):
+      # parse the arguments
+      try:
+        kwargs, tags, args = parse_args(args)
+      except ParserError, e:
+        self._parser.error(e.message)
+
+      # make sure we have some manifests, otherwise it will
+      # be quite boring
+      if not len(args) == 2:
+        HelpCLI(self._parser)(options, ['copy'])
+        return
+
+      # read the manifests
+      # TODO: should probably ensure these exist here
+      manifests = ManifestParser()
+      manifests.read(args[0])
+
+      # print the resultant query
+      manifests.copy(args[1], None, *tags, **kwargs)
+
+
+class CreateCLI(CLICommand):
+    """
+    create a manifest from a list of directories
+    """
+    usage = '%prog [options] create directory <directory> <...>'
+
+    def parser(self):
+        parser = CLICommand.parser(self)
+        parser.add_option('-p', '--pattern', dest='pattern',
+                          help="glob pattern for files")
+        parser.add_option('-i', '--ignore', dest='ignore',
+                          default=[], action='append',
+                          help='directories to ignore')
+        parser.add_option('-w', '--in-place', dest='in_place',
+                          help='Write .ini files in place; filename to write to')
+        return parser
+
+    def __call__(self, _options, args):
+        parser = self.parser()
+        options, args = parser.parse_args(args)
+
+        # need some directories
+        if not len(args):
+            parser.print_usage()
+            return
+
+        # add the directories to the manifest
+        for arg in args:
+            assert os.path.exists(arg)
+            assert os.path.isdir(arg)
+            manifest = convert(args, pattern=options.pattern, ignore=options.ignore,
+                               write=options.in_place)
+        if manifest:
+            print manifest
+
+
+class WriteCLI(CLICommand):
+    """
+    write a manifest based on a query
+    """
+    usage = '%prog [options] write manifest <manifest> -tag1 -tag2 --key1=value1 --key2=value2 ...'
+    def __call__(self, options, args):
+
+        # parse the arguments
+        try:
+            kwargs, tags, args = parse_args(args)
+        except ParserError, e:
+            self._parser.error(e.message)
+
+        # make sure we have some manifests, otherwise it will
+        # be quite boring
+        if not args:
+            HelpCLI(self._parser)(options, ['write'])
+            return
+
+        # read the manifests
+        # TODO: should probably ensure these exist here
+        manifests = ManifestParser()
+        manifests.read(*args)
+
+        # print the resultant query
+        manifests.write(global_tags=tags, global_kwargs=kwargs)
+
+
+class HelpCLI(CLICommand):
+    """
+    get help on a command
+    """
+    usage = '%prog [options] help [command]'
+
+    def __call__(self, options, args):
+        if len(args) == 1 and args[0] in commands:
+            commands[args[0]](self._parser).parser().print_help()
+        else:
+            self._parser.print_help()
+            print '\nCommands:'
+            for command in sorted(commands):
+                print '  %s : %s' % (command, commands[command].__doc__.strip())
+
+class UpdateCLI(CLICommand):
+    """
+    update the tests as listed in a manifest from a directory
+    """
+    usage = '%prog [options] update manifest directory -tag1 -tag2 --key1=value1 --key2=value2 ...'
+
+    def __call__(self, options, args):
+        # parse the arguments
+        try:
+            kwargs, tags, args = parse_args(args)
+        except ParserError, e:
+            self._parser.error(e.message)
+
+        # make sure we have some manifests, otherwise it will
+        # be quite boring
+        if not len(args) == 2:
+            HelpCLI(self._parser)(options, ['update'])
+            return
+
+        # read the manifests
+        # TODO: should probably ensure these exist here
+        manifests = ManifestParser()
+        manifests.read(args[0])
+
+        # print the resultant query
+        manifests.update(args[1], None, *tags, **kwargs)
+
+
+# command -> class mapping
+commands = { 'create': CreateCLI,
+             'help': HelpCLI,
+             'update': UpdateCLI,
+             'write': WriteCLI }
+
+def main(args=sys.argv[1:]):
+    """console_script entry point"""
+
+    # set up an option parser
+    usage = '%prog [options] [command] ...'
+    description = __doc__
+    parser = OptionParser(usage=usage, description=description)
+    parser.add_option('-s', '--strict', dest='strict',
+                      action='store_true', default=False,
+                      help='adhere strictly to errors')
+    parser.disable_interspersed_args()
+
+    options, args = parser.parse_args(args)
+
+    if not args:
+        HelpCLI(parser)(options, args)
+        parser.exit()
+
+    # get the command
+    command = args[0]
+    if command not in commands:
+        parser.error("Command must be one of %s (you gave '%s')" % (', '.join(sorted(commands.keys())), command))
+
+    handler = commands[command](parser)
+    handler(options, args[1:])
+
+if __name__ == '__main__':
+    main()
--- a/testing/mozbase/manifestdestiny/setup.py
+++ b/testing/mozbase/manifestdestiny/setup.py
@@ -36,11 +36,42 @@
 #
 # ***** END LICENSE BLOCK *****
 
 # The real details are in manifestparser.py; this is just a front-end
 # BUT use this file when you want to distribute to python!
 # otherwise setuptools will complain that it can't find setup.py
 # and result in a useless package
 
+from setuptools import setup, find_packages
 import sys
-from manifestparser import SetupCLI
-SetupCLI(None)(None, sys.argv[1:])
+import os
+
+here = os.path.dirname(os.path.abspath(__file__))
+try:
+    filename = os.path.join(here, 'README.txt')
+    description = file(filename).read()
+except:
+    description = ''
+
+PACKAGE_NAME = "ManifestDestiny"
+PACKAGE_VERSION = "0.5.4"
+
+setup(name=PACKAGE_NAME,
+      version=PACKAGE_VERSION,
+      description="Universal manifests for Mozilla test harnesses",
+      long_description=description,
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='mozilla manifests',
+      author='Jeff Hammel',
+      author_email='jhammel@mozilla.com',
+      url='https://github.com/mozilla/mozbase/tree/master/manifestdestiny',
+      license='MPL',
+      zip_safe=False,
+      packages=find_packages(exclude=['legacy']),
+      install_requires=[
+      # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      [console_scripts]
+      manifestparser = manifestparser:main
+      """,
+     )
old mode 100644
new mode 100755
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/README.md
@@ -0,0 +1,5 @@
+[mozdevice](https://github.com/mozilla/mozbase/tree/master/mozdevice) provides
+an interface to interact with a remote device such as an Android phone connected
+to a workstation. Currently there are two implementations of the interface: one
+uses a TCP-based protocol to communicate with a server running on the device,
+another uses Android's adb utility.
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/__init__.py
@@ -0,0 +1,39 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is Mozbase.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Will Lachance <wlachance@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+from devicemanagerADB import DeviceManagerADB
+from devicemanagerSUT import DeviceManagerSUT
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
@@ -0,0 +1,538 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.   
+#
+# The Original Code is Test Automation Framework.
+#
+# The Initial Developer of the Original Code is Joel Maher.
+#
+# Portions created by the Initial Developer are Copyright (C) 2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Joel Maher <joel.maher@gmail.com> (Original Developer)
+#   Clint Talbert <cmtalbert@gmail.com>
+#   Mark Cote <mcote@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import time
+import hashlib
+import socket
+import os
+import re
+
+class FileError(Exception):
+  " Signifies an error which occurs while doing a file operation."
+
+  def __init__(self, msg = ''):
+    self.msg = msg
+
+  def __str__(self):
+    return self.msg
+
+class DMError(Exception):
+  "generic devicemanager exception."
+
+  def __init__(self, msg= ''):
+    self.msg = msg
+
+  def __str__(self):
+    return self.msg
+
+
+class DeviceManager:
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def pushFile(self, localname, destname):
+    assert 0 == 1
+    return False
+
+  # external function
+  # returns:
+  #  success: directory name
+  #  failure: None
+  def mkDir(self, name):
+      assert 0 == 1
+      return None
+
+  # make directory structure on the device
+  # external function
+  # returns:
+  #  success: directory structure that we created
+  #  failure: None
+  def mkDirs(self, filename):
+      assert 0 == 1
+      return None
+
+  # push localDir from host to remoteDir on the device
+  # external function
+  # returns:
+  #  success: remoteDir
+  #  failure: None
+  def pushDir(self, localDir, remoteDir):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def dirExists(self, dirname):
+    assert 0 == 1
+    return False
+
+  # Because we always have / style paths we make this a lot easier with some
+  # assumptions
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def fileExists(self, filepath):
+    assert 0 == 1
+    return False
+
+  # list files on the device, requires cd to directory first
+  # external function
+  # returns:
+  #  success: array of filenames, ['file1', 'file2', ...]
+  #  failure: []
+  def listFiles(self, rootdir):
+    assert 0 == 1
+    return []
+
+  # external function
+  # returns:
+  #  success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
+  #  failure: None
+  def removeFile(self, filename):
+    assert 0 == 1
+    return False
+
+  # does a recursive delete of directory on the device: rm -Rf remoteDir
+  # external function
+  # returns:
+  #  success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
+  #  failure: None
+  def removeDir(self, remoteDir):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: array of process tuples
+  #  failure: []
+  def getProcessList(self):
+    assert 0 == 1
+    return []
+
+  # external function
+  # returns:
+  #  success: pid
+  #  failure: None
+  def fireProcess(self, appname, failIfRunning=False):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: output filename
+  #  failure: None
+  def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False):
+    assert 0 == 1
+    return None
+
+  # loops until 'process' has exited or 'timeout' seconds is reached
+  # loop sleeps for 'interval' seconds between iterations
+  # external function
+  # returns:
+  #  success: [file contents, None]
+  #  failure: [None, None]
+  def communicate(self, process, timeout = 600, interval = 5):
+    timed_out = True
+    if (timeout > 0):
+      total_time = 0
+      while total_time < timeout:
+        time.sleep(interval)
+        if self.processExist(process) == None:
+          timed_out = False
+          break
+        total_time += interval
+
+    if (timed_out == True):
+      return [None, None]
+
+    return [self.getFile(process, "temp.txt"), None]
+
+  # iterates process list and returns pid if exists, otherwise None
+  # external function
+  # returns:
+  #  success: pid
+  #  failure: None
+  def processExist(self, appname):
+    pid = None
+
+    #filter out extra spaces
+    parts = filter(lambda x: x != '', appname.split(' '))
+    appname = ' '.join(parts)
+
+    #filter out the quoted env string if it exists
+    #ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
+    parts = appname.split('"')
+    if (len(parts) > 2):
+      appname = ' '.join(parts[2:]).strip()
+  
+    pieces = appname.split(' ')
+    parts = pieces[0].split('/')
+    app = parts[-1]
+    procre = re.compile('.*' + app + '.*')
+
+    procList = self.getProcessList()
+    if (procList == []):
+      return None
+      
+    for proc in procList:
+      if (procre.match(proc[1])):
+        pid = proc[0]
+        break
+    return pid
+
+  # external function
+  # returns:
+  #  success: output from testagent
+  #  failure: None
+  def killProcess(self, appname):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: filecontents
+  #  failure: None
+  def catFile(self, remoteFile):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: output of pullfile, string
+  #  failure: None
+  def pullFile(self, remoteFile):
+    assert 0 == 1
+    return None
+
+  # copy file from device (remoteFile) to host (localFile)
+  # external function
+  # returns:
+  #  success: output of pullfile, string
+  #  failure: None
+  def getFile(self, remoteFile, localFile = ''):
+    assert 0 == 1
+    return None
+
+  # copy directory structure from device (remoteDir) to host (localDir)
+  # external function
+  # checkDir exists so that we don't create local directories if the
+  # remote directory doesn't exist but also so that we don't call isDir
+  # twice when recursing.
+  # returns:
+  #  success: list of files, string
+  #  failure: None
+  def getDirectory(self, remoteDir, localDir, checkDir=True):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  #  Throws a FileError exception when null (invalid dir/filename)
+  def isDir(self, remotePath):
+    assert 0 == 1
+    return False
+
+  # true/false check if the two files have the same md5 sum
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def validateFile(self, remoteFile, localFile):
+    assert 0 == 1
+    return False
+
+  # return the md5 sum of a remote file
+  # internal function
+  # returns:
+  #  success: MD5 hash for given filename
+  #  failure: None
+  def getRemoteHash(self, filename):
+    assert 0 == 1
+    return None
+
+  # return the md5 sum of a file on the host
+  # internal function
+  # returns:
+  #  success: MD5 hash for given filename
+  #  failure: None
+  def getLocalHash(self, filename):
+    file = open(filename, 'rb')
+    if (file == None):
+      return None
+
+    try:
+      mdsum = hashlib.md5()
+    except:
+      return None
+
+    while 1:
+      data = file.read(1024)
+      if not data:
+        break
+      mdsum.update(data)
+
+    file.close()
+    hexval = mdsum.hexdigest()
+    if (self.debug >= 3): print "local hash returned: '" + hexval + "'"
+    return hexval
+  # Gets the device root for the testing area on the device
+  # For all devices we will use / type slashes and depend on the device-agent
+  # to sort those out.  The agent will return us the device location where we
+  # should store things, we will then create our /tests structure relative to
+  # that returned path.
+  # Structure on the device is as follows:
+  # /tests
+  #       /<fennec>|<firefox>  --> approot
+  #       /profile
+  #       /xpcshell
+  #       /reftest
+  #       /mochitest
+  #
+  # external function
+  # returns:
+  #  success: path for device root
+  #  failure: None
+  def getDeviceRoot(self):
+    assert 0 == 1
+    return None
+
+  # Either we will have /tests/fennec or /tests/firefox but we will never have
+  # both.  Return the one that exists
+  # TODO: ensure we can support org.mozilla.firefox
+  # external function
+  # returns:
+  #  success: path for app root
+  #  failure: None
+  def getAppRoot(self):
+    devroot = self.getDeviceRoot()
+    if (devroot == None):
+      return None
+
+    if (self.dirExists(devroot + '/fennec')):
+      return devroot + '/fennec'
+    elif (self.dirExists(devroot + '/firefox')):
+      return devroot + '/firefox'
+    elif (self.dirExsts('/data/data/org.mozilla.fennec')):
+      return 'org.mozilla.fennec'
+    elif (self.dirExists('/data/data/org.mozilla.firefox')):
+      return 'org.mozilla.firefox'
+    elif (self.dirExists('/data/data/org.mozilla.fennec_aurora')):
+      return 'org.mozilla.fennec_aurora'
+    elif (self.dirExists('/data/data/org.mozilla.firefox_beta')):
+      return 'org.mozilla.firefox_beta'
+
+    # Failure (either not installed or not a recognized platform)
+    return None
+
+  # Gets the directory location on the device for a specific test type
+  # Type is one of: xpcshell|reftest|mochitest
+  # external function
+  # returns:
+  #  success: path for test root
+  #  failure: None
+  def getTestRoot(self, type):
+    devroot = self.getDeviceRoot()
+    if (devroot == None):
+      return None
+
+    if (re.search('xpcshell', type, re.I)):
+      self.testRoot = devroot + '/xpcshell'
+    elif (re.search('?(i)reftest', type)):
+      self.testRoot = devroot + '/reftest'
+    elif (re.search('?(i)mochitest', type)):
+      self.testRoot = devroot + '/mochitest'
+    return self.testRoot
+
+  # Sends a specific process ID a signal code and action.
+  # For Example: SIGINT and SIGDFL to process x
+  def signal(self, processID, signalType, signalAction):
+    # currently not implemented in device agent - todo
+    pass
+
+  # Get a return code from process ending -- needs support on device-agent
+  def getReturnCode(self, processID):
+    # TODO: make this real
+    return 0
+
+  # external function
+  # returns:
+  #  success: output of unzip command
+  #  failure: None
+  def unpackFile(self, filename):
+    return None
+
+  # external function
+  # returns:
+  #  success: status from test agent
+  #  failure: None
+  def reboot(self, ipAddr=None, port=30000):
+    assert 0 == 1
+    return None
+
+  # validate localDir from host to remoteDir on the device
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def validateDir(self, localDir, remoteDir):
+    if (self.debug >= 2): print "validating directory: " + localDir + " to " + remoteDir
+    for root, dirs, files in os.walk(localDir):
+      parts = root.split(localDir)
+      for file in files:
+        remoteRoot = remoteDir + '/' + parts[1]
+        remoteRoot = remoteRoot.replace('/', '/')
+        if (parts[1] == ""): remoteRoot = remoteDir
+        remoteName = remoteRoot + '/' + file
+        if (self.validateFile(remoteName, os.path.join(root, file)) <> True):
+            return False
+    return True
+
+  # Returns information about the device:
+  # Directive indicates the information you want to get, your choices are:
+  # os - name of the os
+  # id - unique id of the device
+  # uptime - uptime of the device
+  # systime - system time of the device
+  # screen - screen resolution
+  # memory - memory stats
+  # process - list of running processes (same as ps)
+  # disk - total, free, available bytes on disk
+  # power - power status (charge, battery temp)
+  # all - all of them - or call it with no parameters to get all the information
+  # returns:
+  #   success: dict of info strings by directive name
+  #   failure: {}
+  def getInfo(self, directive=None):
+    assert 0 == 1
+    return {}
+
+  # external function
+  # returns:
+  #  success: output from agent for inst command
+  #  failure: None
+  def installApp(self, appBundlePath, destPath=None):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: True
+  #  failure: None
+  def uninstallAppAndReboot(self, appName, installPath=None):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: text status from command or callback server
+  #  failure: None
+  def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
+    assert 0 == 1
+    return None
+
+  # external function
+  # returns:
+  #  success: time in ms
+  #  failure: None
+  def getCurrentTime(self):
+    assert 0 == 1
+    return None
+
+
+class NetworkTools:
+  def __init__(self):
+    pass
+
+  # Utilities to get the local ip address
+  def getInterfaceIp(self, ifname):
+    if os.name != "nt":
+      import fcntl
+      import struct
+      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])
+    else:
+      return None
+
+  def getLanIp(self):
+    ip = socket.gethostbyname(socket.gethostname())
+    if ip.startswith("127.") and os.name != "nt":
+      interfaces = ["eth0","eth1","eth2","wlan0","wlan1","wifi0","ath0","ath1","ppp0"]
+      for ifname in interfaces:
+        try:
+          ip = self.getInterfaceIp(ifname)
+          break;
+        except IOError:
+          pass
+    return ip
+
+  # Gets an open port starting with the seed by incrementing by 1 each time
+  def findOpenPort(self, ip, seed):
+    try:
+      s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+      s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+      connected = False
+      if isinstance(seed, basestring):
+        seed = int(seed)
+      maxportnum = seed + 5000 # We will try at most 5000 ports to find an open one
+      while not connected:
+        try:
+          s.bind((ip, seed))
+          connected = True
+          s.close()
+          break
+        except:          
+          if seed > maxportnum:
+            print "Could not find open port after checking 5000 ports"
+          raise
+        seed += 1
+    except:
+      print "Socket error trying to find open port"
+        
+    return seed
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
@@ -0,0 +1,580 @@
+import subprocess
+from devicemanager import DeviceManager, DMError
+import re
+import os
+import sys
+
+class DeviceManagerADB(DeviceManager):
+
+  def __init__(self, host = None, port = 20701, retrylimit = 5, packageName = None):
+    self.host = host
+    self.port = port
+    self.retrylimit = retrylimit
+    self.retries = 0
+    self._sock = None
+    self.useRunAs = False
+    self.packageName = None
+    if packageName == None:
+      if os.getenv('USER'):
+        packageName = 'org.mozilla.fennec_' + os.getenv('USER')
+      else:
+        packageName = 'org.mozilla.fennec_'
+    self.Init(packageName)
+
+  def Init(self, packageName):
+    # Initialization code that may fail: Catch exceptions here to allow
+    # successful initialization even if, for example, adb is not installed.
+    try:
+      self.verifyADB()
+      self.verifyRunAs(packageName)
+    except:
+      self.useRunAs = False
+      self.packageName = None
+    try:
+      # a test to see if we have root privs
+      files = self.listFiles("/data/data")
+      if (len(files) == 1):
+        if (files[0].find("Permission denied") != -1):
+          print "NOT running as root"
+          raise Exception("not running as root")
+    except:
+      try:
+        self.checkCmd(["root"])
+      except:
+        print "restarting as root failed"
+
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def pushFile(self, localname, destname):
+    try:
+      if (os.name == "nt"):
+        destname = destname.replace('\\', '/')
+      if (self.useRunAs):
+        remoteTmpFile = self.tmpDir + "/" + os.path.basename(localname)
+        self.checkCmd(["push", os.path.realpath(localname), remoteTmpFile])
+        self.checkCmdAs(["shell", "cp", remoteTmpFile, destname])
+        self.checkCmd(["shell", "rm", remoteTmpFile])
+      else:
+        self.checkCmd(["push", os.path.realpath(localname), destname])
+      if (self.isDir(destname)):
+        destname = destname + "/" + os.path.basename(localname)
+      self.chmodDir(destname)
+      return True
+    except:
+      return False
+
+  # external function
+  # returns:
+  #  success: directory name
+  #  failure: None
+  def mkDir(self, name):
+    try:
+      self.checkCmdAs(["shell", "mkdir", name])
+      self.chmodDir(name)
+      return name
+    except:
+      return None
+
+  # make directory structure on the device
+  # external function
+  # returns:
+  #  success: directory structure that we created
+  #  failure: None
+  def mkDirs(self, filename):
+    parts = filename.split('/')
+    name = ""
+    for part in parts:
+      if (part == parts[-1]): break
+      if (part != ""):
+        name += '/' + part
+        if (not self.dirExists(name)):
+          if (self.mkDir(name) == None):
+            print "failed making directory: " + str(name)
+            return None
+    return name
+
+  # push localDir from host to remoteDir on the device
+  # external function
+  # returns:
+  #  success: remoteDir
+  #  failure: None
+  def pushDir(self, localDir, remoteDir):
+    # adb "push" accepts a directory as an argument, but if the directory
+    # contains symbolic links, the links are pushed, rather than the linked
+    # files; we push file-by-file to get around this limitation
+    try:
+      if (not self.dirExists(remoteDir)):
+        self.mkDirs(remoteDir+"/x")
+      for root, dirs, files in os.walk(localDir, followlinks='true'):
+        relRoot = os.path.relpath(root, localDir)
+        for file in files:
+          localFile = os.path.join(root, file)
+          remoteFile = remoteDir + "/"
+          if (relRoot!="."):
+            remoteFile = remoteFile + relRoot + "/"
+          remoteFile = remoteFile + file
+          self.pushFile(localFile, remoteFile)
+        for dir in dirs:
+          targetDir = remoteDir + "/"
+          if (relRoot!="."):
+            targetDir = targetDir + relRoot + "/"
+          targetDir = targetDir + dir
+          if (not self.dirExists(targetDir)):
+            self.mkDir(targetDir)
+      self.checkCmdAs(["shell", "chmod", "777", remoteDir])
+      return True
+    except:
+      print "pushing " + localDir + " to " + remoteDir + " failed"
+      return False
+
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def dirExists(self, dirname):
+    return self.isDir(dirname)
+
+  # Because we always have / style paths we make this a lot easier with some
+  # assumptions
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def fileExists(self, filepath):
+    p = self.runCmd(["shell", "ls", "-a", filepath])
+    data = p.stdout.readlines()
+    if (len(data) == 1):
+      if (data[0].rstrip() == filepath):
+        return True
+    return False
+
+  def removeFile(self, filename):
+    return self.runCmd(["shell", "rm", filename]).stdout.read()
+
+  # does a recursive delete of directory on the device: rm -Rf remoteDir
+  # external function
+  # returns:
+  #  success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
+  #  failure: None
+  def removeSingleDir(self, remoteDir):
+    return self.runCmd(["shell", "rmdir", remoteDir]).stdout.read()
+
+  # does a recursive delete of directory on the device: rm -Rf remoteDir
+  # external function
+  # returns:
+  #  success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
+  #  failure: None
+  def removeDir(self, remoteDir):
+      out = ""
+      if (self.isDir(remoteDir)):
+          files = self.listFiles(remoteDir.strip())
+          for f in files:
+              if (self.isDir(remoteDir.strip() + "/" + f.strip())):
+                  out += self.removeDir(remoteDir.strip() + "/" + f.strip())
+              else:
+                  out += self.removeFile(remoteDir.strip() + "/" + f.strip())
+          out += self.removeSingleDir(remoteDir.strip())
+      else:
+          out += self.removeFile(remoteDir.strip())
+      return out
+
+  def isDir(self, remotePath):
+      p = self.runCmd(["shell", "ls", "-a", remotePath])
+      data = p.stdout.readlines()
+      if (len(data) == 0):
+          return True
+      if (len(data) == 1):
+          if (data[0].rstrip() == remotePath):
+              return False
+          if (data[0].find("No such file or directory") != -1):
+              return False
+          if (data[0].find("Not a directory") != -1):
+              return False
+      return True
+
+  def listFiles(self, rootdir):
+      p = self.runCmd(["shell", "ls", "-a", rootdir])
+      data = p.stdout.readlines()
+      if (len(data) == 1):
+          if (data[0] == rootdir):
+              return []
+          if (data[0].find("No such file or directory") != -1):
+              return []
+          if (data[0].find("Not a directory") != -1):
+              return []
+      return data
+
+  # external function
+  # returns:
+  #  success: array of process tuples
+  #  failure: []
+  def getProcessList(self):
+    p = self.runCmd(["shell", "ps"])
+      # first line is the headers
+    p.stdout.readline()
+    proc = p.stdout.readline()
+    ret = []
+    while (proc):
+      els = proc.split()
+      ret.append(list([els[1], els[len(els) - 1], els[0]]))
+      proc =  p.stdout.readline()
+    return ret
+
+  # external function
+  # returns:
+  #  success: pid
+  #  failure: None
+  def fireProcess(self, appname, failIfRunning=False):
+    #strip out env vars
+    parts = appname.split('"');
+    if (len(parts) > 2):
+      parts = parts[2:]
+    return self.launchProcess(parts, failIfRunning)
+
+  # external function
+  # returns:
+  #  success: output filename
+  #  failure: None
+  def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False):
+    acmd = ["shell", "am","start"]
+    cmd = ' '.join(cmd).strip()
+    i = cmd.find(" ")
+    acmd.append("-n")
+    acmd.append(cmd[0:i] + "/.App")
+    acmd.append("--es")
+    acmd.append("args")
+    acmd.append(cmd[i:])
+    print acmd
+    self.checkCmd(acmd)
+    return outputFile;
+
+  # external function
+  # returns:
+  #  success: output from testagent
+  #  failure: None
+  def killProcess(self, appname):
+    procs = self.getProcessList()
+    for (pid, name, user) in procs:
+      if name == appname:
+        p = self.runCmdAs(["shell", "kill", pid])
+        return p.stdout.read()
+    return None
+
+  # external function
+  # returns:
+  #  success: filecontents
+  #  failure: None
+  def catFile(self, remoteFile):
+    #p = self.runCmd(["shell", "cat", remoteFile])
+    #return p.stdout.read()
+    return self.getFile(remoteFile)
+
+  # external function
+  # returns:
+  #  success: output of pullfile, string
+  #  failure: None
+  def pullFile(self, remoteFile):
+    #return self.catFile(remoteFile)
+    return self.getFile(remoteFile)
+
+  # copy file from device (remoteFile) to host (localFile)
+  # external function
+  # returns:
+  #  success: output of pullfile, string
+  #  failure: None
+  def getFile(self, remoteFile, localFile = 'tmpfile_dm_adb'):
+    # TODO: add debug flags and allow for printing stdout
+    # self.runCmd(["pull", remoteFile, localFile])
+    try:
+      self.runCmd(["pull",  remoteFile, localFile]).stdout.read()
+      f = open(localFile)
+      ret = f.read()
+      f.close()
+      return ret;      
+    except:
+      return None
+
+  # copy directory structure from device (remoteDir) to host (localDir)
+  # external function
+  # checkDir exists so that we don't create local directories if the
+  # remote directory doesn't exist but also so that we don't call isDir
+  # twice when recursing.
+  # returns:
+  #  success: list of files, string
+  #  failure: None
+  def getDirectory(self, remoteDir, localDir, checkDir=True):
+    ret = []
+    p = self.runCmd(["pull", remoteDir, localDir])
+    p.stderr.readline()
+    line = p.stderr.readline()
+    while (line):
+      els = line.split()
+      f = els[len(els) - 1]
+      i = f.find(localDir)
+      if (i != -1):
+        if (localDir[len(localDir) - 1] != '/'):
+          i = i + 1
+        f = f[i + len(localDir):]
+      i = f.find("/")
+      if (i > 0):
+        f = f[0:i]
+      ret.append(f)
+      line =  p.stderr.readline()
+    #the last line is a summary
+    if (len(ret) > 0):
+      ret.pop()
+    return ret
+
+
+
+  # true/false check if the two files have the same md5 sum
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def validateFile(self, remoteFile, localFile):
+    return self.getRemoteHash(remoteFile) == self.getLocalHash(localFile)
+
+  # return the md5 sum of a remote file
+  # internal function
+  # returns:
+  #  success: MD5 hash for given filename
+  #  failure: None
+  def getRemoteHash(self, filename):
+    data = p = self.runCmd(["shell", "ls", "-l", filename]).stdout.read()
+    return data.split()[3]
+
+  def getLocalHash(self, filename):
+    data = p = subprocess.Popen(["ls", "-l", filename], stdout=subprocess.PIPE).stdout.read()
+    return data.split()[4]
+
+  # Gets the device root for the testing area on the device
+  # For all devices we will use / type slashes and depend on the device-agent
+  # to sort those out.  The agent will return us the device location where we
+  # should store things, we will then create our /tests structure relative to
+  # that returned path.
+  # Structure on the device is as follows:
+  # /tests
+  #       /<fennec>|<firefox>  --> approot
+  #       /profile
+  #       /xpcshell
+  #       /reftest
+  #       /mochitest
+  #
+  # external function
+  # returns:
+  #  success: path for device root
+  #  failure: None
+  def getDeviceRoot(self):
+    # /mnt/sdcard/tests is preferred to /data/local/tests, but this can be
+    # over-ridden by creating /data/local/tests
+    testRoot = "/data/local/tests"
+    if (self.dirExists(testRoot)):
+      return testRoot
+    root = "/mnt/sdcard"
+    if (not self.dirExists(root)):
+      root = "/data/local"
+    testRoot = root + "/tests"
+    if (not self.dirExists(testRoot)):
+      self.mkDir(testRoot)
+    return testRoot
+
+  # Either we will have /tests/fennec or /tests/firefox but we will never have
+  # both.  Return the one that exists
+  # TODO: ensure we can support org.mozilla.firefox
+  # external function
+  # returns:
+  #  success: path for app root
+  #  failure: None
+  def getAppRoot(self):
+    devroot = self.getDeviceRoot()
+    if (devroot == None):
+      return None
+
+    if (self.dirExists(devroot + '/fennec')):
+      return devroot + '/fennec'
+    elif (self.dirExists(devroot + '/firefox')):
+      return devroot + '/firefox'
+    elif (self.packageName and self.dirExists('/data/data/' + self.packageName)):
+      return '/data/data/' + self.packageName
+
+    # Failure (either not installed or not a recognized platform)
+    print "devicemanagerADB: getAppRoot failed"
+    return None
+
+  # Gets the directory location on the device for a specific test type
+  # Type is one of: xpcshell|reftest|mochitest
+  # external function
+  # returns:
+  #  success: path for test root
+  #  failure: None
+  def getTestRoot(self, type):
+    devroot = self.getDeviceRoot()
+    if (devroot == None):
+      return None
+
+    if (re.search('xpcshell', type, re.I)):
+      self.testRoot = devroot + '/xpcshell'
+    elif (re.search('?(i)reftest', type)):
+      self.testRoot = devroot + '/reftest'
+    elif (re.search('?(i)mochitest', type)):
+      self.testRoot = devroot + '/mochitest'
+    return self.testRoot
+
+
+  # external function
+  # returns:
+  #  success: status from test agent
+  #  failure: None
+  def reboot(self, wait = False):
+    ret = self.runCmd(["reboot"]).stdout.read()
+    if (not wait):
+      return "Success"
+    countdown = 40
+    while (countdown > 0):
+      countdown
+      try:
+        self.checkCmd(["wait-for-device", "shell", "ls", "/sbin"])
+        return ret
+      except:
+        try:
+          self.checkCmd(["root"])
+        except:
+          time.sleep(1)
+          print "couldn't get root"
+    return "Success"
+
+  # external function
+  # returns:
+  #  success: text status from command or callback server
+  #  failure: None
+  def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
+    return self.runCmd(["install", "-r", appBundlePath]).stdout.read()
+
+  # external function
+  # returns:
+  #  success: time in ms
+  #  failure: None
+  def getCurrentTime(self):
+    timestr = self.runCmd(["shell", "date", "+%s"]).stdout.read().strip()
+    if (not timestr or not timestr.isdigit()):
+        return None
+    return str(int(timestr)*1000)
+
+  # Returns information about the device:
+  # Directive indicates the information you want to get, your choices are:
+  # os - name of the os
+  # id - unique id of the device
+  # uptime - uptime of the device
+  # systime - system time of the device
+  # screen - screen resolution
+  # memory - memory stats
+  # process - list of running processes (same as ps)
+  # disk - total, free, available bytes on disk
+  # power - power status (charge, battery temp)
+  # all - all of them - or call it with no parameters to get all the information
+  # returns:
+  #   success: dict of info strings by directive name
+  #   failure: {}
+  def getInfo(self, directive="all"):
+    ret = {}
+    if (directive == "id" or directive == "all"):
+      ret["id"] = self.runCmd(["get-serialno"]).stdout.read()
+    if (directive == "os" or directive == "all"):
+      ret["os"] = self.runCmd(["shell", "getprop", "ro.build.display.id"]).stdout.read()
+    if (directive == "uptime" or directive == "all"):
+      utime = self.runCmd(["shell", "uptime"]).stdout.read()
+      if (not utime):
+        raise DMError("error getting uptime")
+      utime = utime[9:]
+      hours = utime[0:utime.find(":")]
+      utime = utime[utime[1:].find(":") + 2:]
+      minutes = utime[0:utime.find(":")]
+      utime = utime[utime[1:].find(":") +  2:]
+      seconds = utime[0:utime.find(",")]
+      ret["uptime"] = ["0 days " + hours + " hours " + minutes + " minutes " + seconds + " seconds"]
+    if (directive == "process" or directive == "all"):
+      ret["process"] = self.runCmd(["shell", "ps"]).stdout.read()
+    if (directive == "systime" or directive == "all"):
+      ret["systime"] = self.runCmd(["shell", "date"]).stdout.read()
+    print ret
+    return ret
+
+  def runCmd(self, args):
+    args.insert(0, "adb")
+    return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+  def runCmdAs(self, args):
+    if self.useRunAs:
+      args.insert(1, "run-as")
+      args.insert(2, self.packageName)
+    return self.runCmd(args)
+
+  def checkCmd(self, args):
+    args.insert(0, "adb")
+    return subprocess.check_call(args)
+
+  def checkCmdAs(self, args):
+    if (self.useRunAs):
+      args.insert(1, "run-as")
+      args.insert(2, self.packageName)
+    return self.checkCmd(args)
+
+  def chmodDir(self, remoteDir):
+    if (self.isDir(remoteDir)):
+      files = self.listFiles(remoteDir.strip())
+      for f in files:
+        if (self.isDir(remoteDir.strip() + "/" + f.strip())):
+          self.chmodDir(remoteDir.strip() + "/" + f.strip())
+        else:
+          self.checkCmdAs(["shell", "chmod", "777", remoteDir.strip()])
+          print "chmod " + remoteDir.strip()
+      self.checkCmdAs(["shell", "chmod", "777", remoteDir])
+      print "chmod " + remoteDir
+    else:
+      self.checkCmdAs(["shell", "chmod", "777", remoteDir.strip()])
+      print "chmod " + remoteDir.strip()
+
+  def verifyADB(self):
+    # Check to see if adb itself can be executed.
+    try:
+      self.runCmd(["version"])
+    except Exception as (ex):
+      print "unable to execute ADB: ensure Android SDK is installed and adb is in your $PATH"
+    
+  def isCpAvailable(self):
+    # Some Android systems may not have a cp command installed,
+    # or it may not be executable by the user. 
+    data = self.runCmd(["shell", "cp"]).stdout.read()
+    if (re.search('Usage', data)):
+      return True
+    else:
+      print "unable to execute 'cp' on device; consider installing busybox from Android Market"
+      return False
+
+  def verifyRunAs(self, packageName):
+    # If a valid package name is available, and certain other
+    # conditions are met, devicemanagerADB can execute file operations
+    # via the "run-as" command, so that pushed files and directories 
+    # are created by the uid associated with the package, more closely
+    # echoing conditions encountered by Fennec at run time.
+    # Check to see if run-as can be used here, by verifying a 
+    # file copy via run-as.
+    self.useRunAs = False
+    devroot = self.getDeviceRoot()
+    if (packageName and self.isCpAvailable() and devroot):
+      self.tmpDir = devroot + "/tmp"
+      if (not self.dirExists(self.tmpDir)):
+        self.mkDir(self.tmpDir)
+      self.checkCmd(["shell", "run-as", packageName, "mkdir", devroot + "/sanity"])
+      self.checkCmd(["push", os.path.abspath(sys.argv[0]), self.tmpDir + "/tmpfile"])
+      self.checkCmd(["shell", "run-as", packageName, "cp", self.tmpDir + "/tmpfile", devroot + "/sanity"])
+      if (self.fileExists(devroot + "/sanity/tmpfile")):
+        print "will execute commands via run-as " + packageName
+        self.packageName = packageName
+        self.useRunAs = True
+      self.checkCmd(["shell", "rm", devroot + "/tmp/tmpfile"])
+      self.checkCmd(["shell", "run-as", packageName, "rm", "-r", devroot + "/sanity"])
+      
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
@@ -0,0 +1,1239 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.   
+#
+# The Original Code is Test Automation Framework.
+#
+# The Initial Developer of the Original Code is Joel Maher.
+#
+# Portions created by the Initial Developer are Copyright (C) 2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Joel Maher <joel.maher@gmail.com> (Original Developer)
+#   Clint Talbert <cmtalbert@gmail.com>
+#   Mark Cote <mcote@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import socket
+import SocketServer
+import time, datetime
+import os
+import re
+import hashlib
+import subprocess
+from threading import Thread
+import traceback
+import sys
+from devicemanager import DeviceManager, DMError, FileError, NetworkTools
+
+class DeviceManagerSUT(DeviceManager):
+  host = ''
+  port = 0
+  debug = 2 
+  retries = 0
+  tempRoot = os.getcwd()
+  base_prompt = '$>'
+  base_prompt_re = '\$\>'
+  prompt_sep = '\x00'
+  prompt_regex = '.*(' + base_prompt_re + prompt_sep + ')'
+  agentErrorRE = re.compile('^##AGENT-WARNING##.*')
+
+  # TODO: member variable to indicate error conditions.
+  # This should be set to a standard error from the errno module.
+  # So, for example, when an error occurs because of a missing file/directory,
+  # before returning, the function would do something like 'self.error = errno.ENOENT'.
+  # The error would be set where appropriate--so sendCMD() could set socket errors,
+  # pushFile() and other file-related commands could set filesystem errors, etc.
+
+  def __init__(self, host, port = 20701, retrylimit = 5):
+    self.host = host
+    self.port = port
+    self.retrylimit = retrylimit
+    self.retries = 0
+    self._sock = None
+    self.getDeviceRoot()
+
+  def cmdNeedsResponse(self, cmd):
+    """ Not all commands need a response from the agent:
+        * if the cmd matches the pushRE then it is the first half of push
+          and therefore we want to wait until the second half before looking
+          for a response
+        * rebt obviously doesn't get a response
+        * uninstall performs a reboot to ensure starting in a clean state and
+          so also doesn't look for a response
+    """
+    noResponseCmds = [re.compile('^push .*$'),
+                      re.compile('^rebt'),
+                      re.compile('^uninst .*$'),
+                      re.compile('^pull .*$')]
+
+    for c in noResponseCmds:
+      if (c.match(cmd)):
+        return False
+    
+    # If the command is not in our list, then it gets a response
+    return True
+
+  def shouldCmdCloseSocket(self, cmd):
+    """ Some commands need to close the socket after they are sent:
+    * push
+    * rebt
+    * uninst
+    * quit
+    """
+    
+    socketClosingCmds = [re.compile('^push .*$'),
+                         re.compile('^quit.*'),
+                         re.compile('^rebt.*'),
+                         re.compile('^uninst .*$')]
+
+    for c in socketClosingCmds:
+      if (c.match(cmd)):
+        return True
+
+    return False
+
+  # convenience function to enable checks for agent errors
+  def verifySendCMD(self, cmdline, newline = True):
+    return self.sendCMD(cmdline, newline, False)
+
+
+  #
+  # create a wrapper for sendCMD that loops up to self.retrylimit iterations.
+  # this allows us to move the retry logic outside of the _doCMD() to make it 
+  # easier for debugging in the future.
+  # note that since cmdline is a list of commands, they will all be retried if
+  # one fails.  this is necessary in particular for pushFile(), where we don't want
+  # to accidentally send extra data if a failure occurs during data transmission.
+  #
+  def sendCMD(self, cmdline, newline = True, ignoreAgentErrors = True):
+    done = False
+    while (not done):
+      retVal = self._doCMD(cmdline, newline)
+      if (retVal is None):
+        self.retries += 1
+      else:
+        self.retries = 0
+        if ignoreAgentErrors == False:
+          if (self.agentErrorRE.match(retVal)):
+            raise DMError("error on the agent executing '%s'" % cmdline)
+        return retVal
+
+      if (self.retries >= self.retrylimit):
+        done = True
+
+    raise DMError("unable to connect to %s after %s attempts" % (self.host, self.retrylimit))        
+
+  def _doCMD(self, cmdline, newline = True):
+    promptre = re.compile(self.prompt_regex + '$')
+    data = ""
+    shouldCloseSocket = False
+    recvGuard = 1000
+
+    if (self._sock == None):
+      try:
+        if (self.debug >= 1):
+          print "reconnecting socket"
+        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+      except:
+        self._sock = None
+        if (self.debug >= 2):
+          print "unable to create socket"
+        return None
+      
+      try:
+        self._sock.connect((self.host, int(self.port)))
+        self._sock.recv(1024)
+      except:
+        self._sock.close()
+        self._sock = None
+        if (self.debug >= 2):
+          print "unable to connect socket"
+        return None
+    
+    for cmd in cmdline:
+      if newline: cmd += '\r\n'
+      
+      try:
+        numbytes = self._sock.send(cmd)
+        if (numbytes != len(cmd)):
+          print "ERROR: our cmd was " + str(len(cmd)) + " bytes and we only sent " + str(numbytes)
+          return None
+        if (self.debug >= 4): print "send cmd: " + str(cmd)
+      except:
+        self._sock.close()
+        self._sock = None
+        return None
+      
+      # Check if the command should close the socket
+      shouldCloseSocket = self.shouldCmdCloseSocket(cmd)
+
+      # Handle responses from commands
+      if (self.cmdNeedsResponse(cmd)):
+        found = False
+        loopguard = 0
+
+        while (found == False and (loopguard < recvGuard)):
+          temp = ''
+          if (self.debug >= 4): print "recv'ing..."
+
+          # Get our response
+          try:
+            temp = self._sock.recv(1024)
+            if (self.debug >= 4): print "response: " + str(temp)
+          except:
+            self._sock.close()
+            self._sock = None
+            return None
+
+          # If something goes wrong in the agent it will send back a string that
+          # starts with '##AGENT-ERROR##'
+          if (self.agentErrorRE.match(temp)):
+            data = temp
+            break
+
+          lines = temp.split('\n')
+
+          for line in lines:
+            if (promptre.match(line)):
+              found = True
+          data += temp
+
+          # If we violently lose the connection to the device, this loop tends to spin,
+          # this guard prevents that
+          if (temp == ''):
+            loopguard += 1
+
+    if (shouldCloseSocket == True):
+      try:
+        self._sock.close()
+        self._sock = None
+      except:
+        self._sock = None
+        return None
+
+    return data
+  
+  # internal function
+  # take a data blob and strip instances of the prompt '$>\x00'
+  def stripPrompt(self, data):
+    promptre = re.compile(self.prompt_regex + '.*')
+    retVal = []
+    lines = data.split('\n')
+    for line in lines:
+      try:
+        while (promptre.match(line)):
+          pieces = line.split(self.prompt_sep)
+          index = pieces.index('$>')
+          pieces.pop(index)
+          line = self.prompt_sep.join(pieces)
+      except(ValueError):
+        pass
+      retVal.append(line)
+
+    return '\n'.join(retVal)
+  
+
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def pushFile(self, localname, destname):
+    if (os.name == "nt"):
+      destname = destname.replace('\\', '/')
+
+    if (self.debug >= 3): print "in push file with: " + localname + ", and: " + destname
+    if (self.dirExists(destname)):
+      if (not destname.endswith('/')):
+        destname = destname + '/'
+      destname = destname + os.path.basename(localname)
+    if (self.validateFile(destname, localname) == True):
+      if (self.debug >= 3): print "files are validated"
+      return True
+
+    if self.mkDirs(destname) == None:
+      print "unable to make dirs: " + destname
+      return False
+
+    if (self.debug >= 3): print "sending: push " + destname
+    
+    filesize = os.path.getsize(localname)
+    f = open(localname, 'rb')
+    data = f.read()
+    f.close()
+
+    try:
+      retVal = self.verifySendCMD(['push ' + destname + ' ' + str(filesize) + '\r\n', data], newline = False)
+    except(DMError):
+      retVal = False
+  
+    if (self.debug >= 3): print "push returned: " + str(retVal)
+
+    validated = False
+    if (retVal):
+      retline = self.stripPrompt(retVal).strip() 
+      if (retline == None):
+        # Then we failed to get back a hash from agent, try manual validation
+        validated = self.validateFile(destname, localname)
+      else:
+        # Then we obtained a hash from push
+        localHash = self.getLocalHash(localname)
+        if (str(localHash) == str(retline)):
+          validated = True
+    else:
+      # We got nothing back from sendCMD, try manual validation
+      validated = self.validateFile(destname, localname)
+
+    if (validated):
+      if (self.debug >= 3): print "Push File Validated!"
+      return True
+    else:
+      if (self.debug >= 2): print "Push File Failed to Validate!"
+      return False
+  
+  # external function
+  # returns:
+  #  success: directory name
+  #  failure: None
+  def mkDir(self, name):
+    if (self.dirExists(name)):
+      return name
+    else:
+      try:
+        retVal = self.verifySendCMD(['mkdr ' + name])
+      except(DMError):
+        retVal = None
+      return retVal
+
+  # make directory structure on the device
+  # external function
+  # returns:
+  #  success: directory structure that we created
+  #  failure: None
+  def mkDirs(self, filename):
+    parts = filename.split('/')
+    name = ""
+    for part in parts:
+      if (part == parts[-1]): break
+      if (part != ""):
+        name += '/' + part
+        if (self.mkDir(name) == None):
+          print "failed making directory: " + str(name)
+          return None
+    return name
+
+  # push localDir from host to remoteDir on the device
+  # external function
+  # returns:
+  #  success: remoteDir
+  #  failure: None
+  def pushDir(self, localDir, remoteDir):
+    if (self.debug >= 2): print "pushing directory: %s to %s" % (localDir, remoteDir)
+    for root, dirs, files in os.walk(localDir):
+      parts = root.split(localDir)
+      for file in files:
+        remoteRoot = remoteDir + '/' + parts[1]
+        if (remoteRoot.endswith('/')):
+          remoteName = remoteRoot + file
+        else:
+          remoteName = remoteRoot + '/' + file
+        if (parts[1] == ""): remoteRoot = remoteDir
+        if (self.pushFile(os.path.join(root, file), remoteName) == False):
+          # retry once
+          self.removeFile(remoteName)
+          if (self.pushFile(os.path.join(root, file), remoteName) == False):
+            return None
+    return remoteDir
+
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def dirExists(self, dirname):
+    match = ".*" + dirname + "$"
+    dirre = re.compile(match)
+    try:
+      data = self.verifySendCMD(['cd ' + dirname, 'cwd'])
+    except(DMError):
+      return False
+
+    retVal = self.stripPrompt(data)
+    data = retVal.split('\n')
+    found = False
+    for d in data:
+      if (dirre.match(d)): 
+        found = True
+
+    return found
+
+  # Because we always have / style paths we make this a lot easier with some
+  # assumptions
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def fileExists(self, filepath):
+    s = filepath.split('/')
+    containingpath = '/'.join(s[:-1])
+    listfiles = self.listFiles(containingpath)
+    for f in listfiles:
+      if (f == s[-1]):
+        return True
+    return False
+
+  # list files on the device, requires cd to directory first
+  # external function
+  # returns:
+  #  success: array of filenames, ['file1', 'file2', ...]
+  #  failure: []
+  def listFiles(self, rootdir):
+    rootdir = rootdir.rstrip('/')
+    if (self.dirExists(rootdir) == False):
+      return []
+    try:
+      data = self.verifySendCMD(['cd ' + rootdir, 'ls'])
+    except(DMError):
+      return []
+
+    retVal = self.stripPrompt(data)
+    files = filter(lambda x: x, retVal.split('\n'))
+    if len(files) == 1 and files[0] == '<empty>':
+      # special case on the agent: empty directories return just the string "<empty>"
+      return []
+    return files
+
+  # external function
+  # returns:
+  #  success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
+  #  failure: None
+  def removeFile(self, filename):
+    if (self.debug>= 2): print "removing file: " + filename
+    try:
+      retVal = self.verifySendCMD(['rm ' + filename])
+    except(DMError):
+      return None
+
+    return retVal
+  
+  # does a recursive delete of directory on the device: rm -Rf remoteDir
+  # external function
+  # returns:
+  #  success: output of telnet, i.e. "removing file: /mnt/sdcard/tests/test.txt"
+  #  failure: None
+  def removeDir(self, remoteDir):
+    try:
+      retVal = self.verifySendCMD(['rmdr ' + remoteDir])
+    except(DMError):
+      return None
+
+    return retVal
+
+  # external function
+  # returns:
+  #  success: array of process tuples
+  #  failure: []
+  def getProcessList(self):
+    try:
+      data = self.verifySendCMD(['ps'])
+    except DMError:
+      return []
+
+    retVal = self.stripPrompt(data)
+    lines = retVal.split('\n')
+    files = []
+    for line in lines:
+      if (line.strip() != ''):
+        pidproc = line.strip().split()
+        if (len(pidproc) == 2):
+          files += [[pidproc[0], pidproc[1]]]
+        elif (len(pidproc) == 3):
+          #android returns <userID> <procID> <procName>
+          files += [[pidproc[1], pidproc[2], pidproc[0]]]     
+    return files
+
+  # external function
+  # returns:
+  #  success: pid
+  #  failure: None
+  def fireProcess(self, appname, failIfRunning=False):
+    if (not appname):
+      if (self.debug >= 1): print "WARNING: fireProcess called with no command to run"
+      return None
+
+    if (self.debug >= 2): print "FIRE PROC: '" + appname + "'"
+
+    if (self.processExist(appname) != None):
+      print "WARNING: process %s appears to be running already\n" % appname
+      if (failIfRunning):
+        return None
+    
+    try:
+      data = self.verifySendCMD(['exec ' + appname])
+    except(DMError):
+      return None
+
+    # wait up to 30 seconds for process to start up
+    timeslept = 0
+    while (timeslept <= 30):
+      process = self.processExist(appname)
+      if (process is not None):
+        break
+      time.sleep(3)
+      timeslept += 3
+
+    if (self.debug >= 4): print "got pid: %s for process: %s" % (process, appname)
+    return process
+
+  # external function
+  # returns:
+  #  success: output filename
+  #  failure: None
+  def launchProcess(self, cmd, outputFile = "process.txt", cwd = '', env = '', failIfRunning=False):
+    if not cmd:
+      if (self.debug >= 1): print "WARNING: launchProcess called without command to run"
+      return None
+
+    cmdline = subprocess.list2cmdline(cmd)
+    if (outputFile == "process.txt" or outputFile == None):
+      outputFile = self.getDeviceRoot();
+      if outputFile is None:
+        return None
+      outputFile += "/process.txt"
+      cmdline += " > " + outputFile
+    
+    # Prepend our env to the command 
+    cmdline = '%s %s' % (self.formatEnvString(env), cmdline)
+
+    if self.fireProcess(cmdline, failIfRunning) is None:
+      return None
+    return outputFile
+  
+  # iterates process list and returns pid if exists, otherwise None
+  # external function
+  # returns:
+  #  success: pid
+  #  failure: None
+  def processExist(self, appname):
+    pid = None
+
+    #filter out extra spaces
+    parts = filter(lambda x: x != '', appname.split(' '))
+    appname = ' '.join(parts)
+
+    #filter out the quoted env string if it exists
+    #ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
+    parts = appname.split('"')
+    if (len(parts) > 2):
+      appname = ' '.join(parts[2:]).strip()
+  
+    pieces = appname.split(' ')
+    parts = pieces[0].split('/')
+    app = parts[-1]
+    procre = re.compile('.*' + app + '.*')
+
+    procList = self.getProcessList()
+    if (procList == []):
+      return None
+      
+    for proc in procList:
+      if (procre.match(proc[1])):
+        pid = proc[0]
+        break
+    return pid
+
+  # external function
+  # returns:
+  #  success: output from testagent
+  #  failure: None
+  def killProcess(self, appname):
+    try:
+      data = self.verifySendCMD(['kill ' + appname])
+    except(DMError):
+      return None
+
+    return data
+
+  # external function
+  # returns:
+  #  success: tmpdir, string
+  #  failure: None
+  def getTempDir(self):
+    try:
+      data = self.verifySendCMD(['tmpd'])
+    except(DMError):
+      return None
+
+    return self.stripPrompt(data).strip('\n')
+
+  # external function
+  # returns:
+  #  success: filecontents
+  #  failure: None
+  def catFile(self, remoteFile):
+    try:
+      data = self.verifySendCMD(['cat ' + remoteFile])
+    except(DMError):
+      return None
+
+    return self.stripPrompt(data)
+  
+  # external function
+  # returns:
+  #  success: output of pullfile, string
+  #  failure: None
+  def pullFile(self, remoteFile):
+    """Returns contents of remoteFile using the "pull" command.
+    The "pull" command is different from other commands in that DeviceManager
+    has to read a certain number of bytes instead of just reading to the
+    next prompt.  This is more robust than the "cat" command, which will be
+    confused if the prompt string exists within the file being catted.
+    However it means we can't use the response-handling logic in sendCMD().
+    """
+    
+    def err(error_msg):
+        err_str = 'error returned from pull: %s' % error_msg
+        print err_str
+        self._sock = None
+        raise FileError(err_str) 
+
+    # FIXME: We could possibly move these socket-reading functions up to
+    # the class level if we wanted to refactor sendCMD().  For now they are
+    # only used to pull files.
+    
+    def uread(to_recv, error_msg):
+      """ unbuffered read """
+      try:
+        data = self._sock.recv(to_recv)
+        if not data:
+          err(error_msg)
+          return None
+        return data
+      except:
+        err(error_msg)
+        return None
+
+    def read_until_char(c, buffer, error_msg):
+      """ read until 'c' is found; buffer rest """
+      while not '\n' in buffer:
+        data = uread(1024, error_msg)
+        if data == None:
+          err(error_msg)
+          return ('', '', '')
+        buffer += data
+      return buffer.partition(c)
+
+    def read_exact(total_to_recv, buffer, error_msg):
+      """ read exact number of 'total_to_recv' bytes """
+      while len(buffer) < total_to_recv:
+        to_recv = min(total_to_recv - len(buffer), 1024)
+        data = uread(to_recv, error_msg)
+        if data == None:
+          return None
+        buffer += data
+      return buffer
+
+    prompt = self.base_prompt + self.prompt_sep
+    buffer = ''
+    
+    # expected return value:
+    # <filename>,<filesize>\n<filedata>
+    # or, if error,
+    # <filename>,-1\n<error message>
+    try:
+      data = self.verifySendCMD(['pull ' + remoteFile])
+    except(DMError):
+      return None
+
+    # read metadata; buffer the rest
+    metadata, sep, buffer = read_until_char('\n', buffer, 'could not find metadata')
+    if not metadata:
+      return None
+    if self.debug >= 3:
+      print 'metadata: %s' % metadata
+
+    filename, sep, filesizestr = metadata.partition(',')
+    if sep == '':
+      err('could not find file size in returned metadata')
+      return None
+    try:
+        filesize = int(filesizestr)
+    except ValueError:
+      err('invalid file size in returned metadata')
+      return None
+
+    if filesize == -1:
+      # read error message
+      error_str, sep, buffer = read_until_char('\n', buffer, 'could not find error message')
+      if not error_str:
+        return None
+      # prompt should follow
+      read_exact(len(prompt), buffer, 'could not find prompt')
+      print "DeviceManager: error pulling file '%s': %s" % (remoteFile, error_str)
+      return None
+
+    # read file data
+    total_to_recv = filesize + len(prompt)
+    buffer = read_exact(total_to_recv, buffer, 'could not get all file data')
+    if buffer == None:
+      return None
+    if buffer[-len(prompt):] != prompt:
+      err('no prompt found after file data--DeviceManager may be out of sync with agent')
+      return buffer
+    return buffer[:-len(prompt)]
+
+  # copy file from device (remoteFile) to host (localFile)
+  # external function
+  # returns:
+  #  success: output of pullfile, string
+  #  failure: None
+  def getFile(self, remoteFile, localFile = ''):
+    if localFile == '':
+      localFile = os.path.join(self.tempRoot, "temp.txt")
+  
+    try:
+      retVal = self.pullFile(remoteFile)
+    except:
+      return None
+      
+    if (retVal is None):
+      return None
+
+    fhandle = open(localFile, 'wb')
+    fhandle.write(retVal)
+    fhandle.close()
+    if not self.validateFile(remoteFile, localFile):
+      print 'failed to validate file when downloading %s!' % remoteFile
+      return None
+    return retVal
+
+  # copy directory structure from device (remoteDir) to host (localDir)
+  # external function
+  # checkDir exists so that we don't create local directories if the
+  # remote directory doesn't exist but also so that we don't call isDir
+  # twice when recursing.
+  # returns:
+  #  success: list of files, string
+  #  failure: None
+  def getDirectory(self, remoteDir, localDir, checkDir=True):
+    if (self.debug >= 2): print "getting files in '" + remoteDir + "'"
+    if checkDir:
+      try:
+        is_dir = self.isDir(remoteDir)
+      except FileError:
+        return None
+      if not is_dir:
+        return None
+        
+    filelist = self.listFiles(remoteDir)
+    if (self.debug >= 3): print filelist
+    if not os.path.exists(localDir):
+      os.makedirs(localDir)
+
+    for f in filelist:
+      if f == '.' or f == '..':
+        continue
+      remotePath = remoteDir + '/' + f
+      localPath = os.path.join(localDir, f)
+      try:
+        is_dir = self.isDir(remotePath)
+      except FileError:
+        print 'isdir failed on file "%s"; continuing anyway...' % remotePath
+        continue
+      if is_dir:
+        if (self.getDirectory(remotePath, localPath, False) == None):
+          print 'failed to get directory "%s"' % remotePath
+          return None
+      else:
+        # It's sometimes acceptable to have getFile() return None, such as
+        # when the agent encounters broken symlinks.
+        # FIXME: This should be improved so we know when a file transfer really
+        # failed.
+        if self.getFile(remotePath, localPath) == None:
+          print 'failed to get file "%s"; continuing anyway...' % remotePath 
+    return filelist
+
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  #  Throws a FileError exception when null (invalid dir/filename)
+  def isDir(self, remotePath):
+    try:
+      data = self.verifySendCMD(['isdir ' + remotePath])
+    except(DMError):
+      # normally there should be no error here; a nonexistent file/directory will
+      # return the string "<filename>: No such file or directory".
+      # However, I've seen AGENT-WARNING returned before. 
+      return False
+    retVal = self.stripPrompt(data).strip()
+    if not retVal:
+      raise FileError('isdir returned null')
+    return retVal == 'TRUE'
+
+  # true/false check if the two files have the same md5 sum
+  # external function
+  # returns:
+  #  success: True
+  #  failure: False
+  def validateFile(self, remoteFile, localFile):
+    remoteHash = self.getRemoteHash(remoteFile)
+    localHash = self.getLocalHash(localFile)
+
+    if (remoteHash == None):
+      return False
+
+    if (remoteHash == localHash):
+      return True
+
+    return False
+  
+  # return the md5 sum of a remote file
+  # internal function
+  # returns:
+  #  success: MD5 hash for given filename
+  #  failure: None
+  def getRemoteHash(self, filename):
+    try:
+      data = self.verifySendCMD(['hash ' + filename])
+    except(DMError):
+      return None
+
+    retVal = self.stripPrompt(data)
+    if (retVal != None):
+      retVal = retVal.strip('\n')
+    if (self.debug >= 3): print "remote hash returned: '" + retVal + "'"
+    return retVal
+    
+  # Gets the device root for the testing area on the device
+  # For all devices we will use / type slashes and depend on the device-agent
+  # to sort those out.  The agent will return us the device location where we
+  # should store things, we will then create our /tests structure relative to
+  # that returned path.
+  # Structure on the device is as follows:
+  # /tests
+  #       /<fennec>|<firefox>  --> approot
+  #       /profile
+  #       /xpcshell
+  #       /reftest
+  #       /mochitest
+  #
+  # external function
+  # returns:
+  #  success: path for device root
+  #  failure: None
+  def getDeviceRoot(self):
+    try:
+      data = self.verifySendCMD(['testroot'])
+    except:
+      return None
+  
+    deviceRoot = self.stripPrompt(data).strip('\n') + '/tests'
+
+    if (not self.dirExists(deviceRoot)):
+      if (self.mkDir(deviceRoot) == None):
+        return None
+
+    return deviceRoot
+
+  # external function
+  # returns:
+  #  success: output of unzip command
+  #  failure: None
+  def unpackFile(self, filename):
+    devroot = self.getDeviceRoot()
+    if (devroot == None):
+      return None
+
+    dir = ''
+    parts = filename.split('/')
+    if (len(parts) > 1):
+      if self.fileExists(filename):
+        dir = '/'.join(parts[:-1])
+    elif self.fileExists('/' + filename):
+      dir = '/' + filename
+    elif self.fileExists(devroot + '/' + filename):
+      dir = devroot + '/' + filename
+    else:
+      return None
+
+    try:
+      data = self.verifySendCMD(['cd ' + dir, 'unzp ' + filename])
+    except(DMError):
+      return None
+
+    return data
+
+  # external function
+  # returns:
+  #  success: status from test agent
+  #  failure: None
+  def reboot(self, ipAddr=None, port=30000):
+    cmd = 'rebt'   
+
+    if (self.debug > 3): print "INFO: sending rebt command"
+    callbacksvrstatus = None    
+
+    if (ipAddr is not None):
+    #create update.info file:
+      try:
+        destname = '/data/data/com.mozilla.SUTAgentAndroid/files/update.info'
+        data = "%s,%s\rrebooting\r" % (ipAddr, port)
+        self.verifySendCMD(['push ' + destname + ' ' + str(len(data)) + '\r\n', data], newline = False)
+      except(DMError):
+        return None
+
+      ip, port = self.getCallbackIpAndPort(ipAddr, port)
+      cmd += " %s %s" % (ip, port)
+      # Set up our callback server
+      callbacksvr = callbackServer(ip, port, self.debug)
+
+    try:
+      status = self.verifySendCMD([cmd])
+    except(DMError):
+      return None
+
+    if (ipAddr is not None):
+      status = callbacksvr.disconnect()
+
+    if (self.debug > 3): print "INFO: rebt- got status back: " + str(status)
+    return status
+
+  # Returns information about the device:
+  # Directive indicates the information you want to get, your choices are:
+  # os - name of the os
+  # id - unique id of the device
+  # uptime - uptime of the device
+  # systime - system time of the device
+  # screen - screen resolution
+  # memory - memory stats
+  # process - list of running processes (same as ps)
+  # disk - total, free, available bytes on disk
+  # power - power status (charge, battery temp)
+  # all - all of them - or call it with no parameters to get all the information
+  # returns:
+  #   success: dict of info strings by directive name
+  #   failure: {}
+  def getInfo(self, directive=None):
+    data = None
+    result = {}
+    collapseSpaces = re.compile('  +')
+
+    directives = ['os', 'id','uptime','systime','screen','memory','process',
+                  'disk','power']
+    if (directive in directives):
+      directives = [directive]
+
+    for d in directives:
+      data = self.verifySendCMD(['info ' + d])
+      if (data is None):
+        continue
+      data = self.stripPrompt(data)
+      data = collapseSpaces.sub(' ', data)
+      result[d] = data.split('\n')
+
+    # Get rid of any 0 length members of the arrays
+    for k, v in result.iteritems():
+      result[k] = filter(lambda x: x != '', result[k])
+    
+    # Format the process output
+    if 'process' in result:
+      proclist = []
+      for l in result['process']:
+        if l:
+          proclist.append(l.split('\t'))
+      result['process'] = proclist
+
+    if (self.debug >= 3): print "results: " + str(result)
+    return result
+
+  """
+  Installs the application onto the device
+  Application bundle - path to the application bundle on the device
+  Destination - destination directory of where application should be
+                installed to (optional)
+  Returns None for success, or output if known failure
+  """
+  # external function
+  # returns:
+  #  success: output from agent for inst command
+  #  failure: None
+  def installApp(self, appBundlePath, destPath=None):
+    cmd = 'inst ' + appBundlePath
+    if destPath:
+      cmd += ' ' + destPath
+    try:
+      data = self.verifySendCMD([cmd])
+    except(DMError):
+      return None
+
+    f = re.compile('Failure')
+    for line in data.split():
+      if (f.match(line)):
+        return data
+    return None
+
+  """
+  Uninstalls the named application from device and causes a reboot.
+  Takes an optional argument of installation path - the path to where the application
+  was installed.
+  Returns True, but it doesn't mean anything other than the command was sent,
+  the reboot happens and we don't know if this succeeds or not.
+  """
+  # external function
+  # returns:
+  #  success: True
+  #  failure: None
+  def uninstallAppAndReboot(self, appName, installPath=None):
+    cmd = 'uninst ' + appName
+    if installPath:
+      cmd += ' ' + installPath
+    try:
+      data = self.verifySendCMD([cmd])
+    except(DMError):
+      return None
+
+    if (self.debug > 3): print "uninstallAppAndReboot: " + str(data)
+    return True
+
+  """
+  Updates the application on the device.
+  Application bundle - path to the application bundle on the device
+  Process name of application - used to end the process if the applicaiton is
+                                currently running
+  Destination - Destination directory to where the application should be
+                installed (optional)
+  ipAddr - IP address to await a callback ping to let us know that the device has updated
+           properly - defaults to current IP.
+  port - port to await a callback ping to let us know that the device has updated properly
+         defaults to 30000, and counts up from there if it finds a conflict
+  Returns True if succeeds, False if not
+  """
+  # external function
+  # returns:
+  #  success: text status from command or callback server
+  #  failure: None
+  def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
+    status = None
+    cmd = 'updt '
+    if (processName == None):
+      # Then we pass '' for processName
+      cmd += "'' " + appBundlePath
+    else:
+      cmd += processName + ' ' + appBundlePath
+
+    if (destPath):
+      cmd += " " + destPath
+
+    if (ipAddr is not None):
+      ip, port = self.getCallbackIpAndPort(ipAddr, port)
+      cmd += " %s %s" % (ip, port)
+      # Set up our callback server
+      callbacksvr = callbackServer(ip, port, self.debug)
+
+    if (self.debug >= 3): print "INFO: updateApp using command: " + str(cmd)
+
+    try:
+      status = self.verifySendCMD([cmd])
+    except(DMError):
+      return None
+
+    if ipAddr is not None:
+      status = callbacksvr.disconnect()
+
+    if (self.debug >= 3): print "INFO: updateApp: got status back: " + str(status)
+
+    return status
+
+  """
+    return the current time on the device
+  """
+  # external function
+  # returns:
+  #  success: time in ms
+  #  failure: None
+  def getCurrentTime(self):
+    try:
+      data = self.verifySendCMD(['clok'])
+    except(DMError):
+      return None
+
+    return self.stripPrompt(data).strip('\n')
+
+  """
+    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!
+  """
+  # external function
+  # returns:
+  #  success: output of unzip command
+  #  failure: None
+  def unpackFile(self, filename):
+    devroot = self.getDeviceRoot()
+    if (devroot == None):
+      return None
+
+    dir = ''
+    parts = filename.split('/')
+    if (len(parts) > 1):
+      if self.fileExists(filename):
+        dir = '/'.join(parts[:-1])
+    elif self.fileExists('/' + filename):
+      dir = '/' + filename
+    elif self.fileExists(devroot + '/' + filename):
+      dir = devroot + '/' + filename
+    else:
+      return None
+
+    try:
+      data = self.verifySendCMD(['cd ' + dir, 'unzp ' + filename])
+    except(DMError):
+      return None
+
+    return data
+
+  def getCallbackIpAndPort(self, aIp, aPort):
+    ip = aIp
+    nettools = NetworkTools()
+    if (ip == None):
+      ip = nettools.getLanIp()
+    if (aPort != None):
+      port = nettools.findOpenPort(ip, aPort)
+    else:
+      port = nettools.findOpenPort(ip, 30000)
+    return ip, port
+
+  """
+    Returns a properly formatted env string for the agent.
+    Input - env, which is either None, '', or a dict
+    Output - a quoted string of the form: '"envvar1=val1,envvar2=val2..."'
+    If env is None or '' return '' (empty quoted string)
+  """
+  def formatEnvString(self, env):
+    if (env == None or env == ''):
+      return ''
+
+    retVal = '"%s"' % ','.join(map(lambda x: '%s=%s' % (x[0], x[1]), env.iteritems()))
+    if (retVal == '""'):
+      return ''
+
+    return retVal
+
+  """
+    adjust the screen resolution on the device, REBOOT REQUIRED
+    NOTE: this only works on a tegra ATM
+    success: True
+    failure: False
+
+    supported resolutions: 640x480, 800x600, 1024x768, 1152x864, 1200x1024, 1440x900, 1680x1050, 1920x1080
+  """
+  def adjustResolution(self, width=1680, height=1050, type='hdmi'):
+    if self.getInfo('os')['os'][0].split()[0] != 'harmony-eng':
+      if (self.debug >= 2): print "WARNING: unable to adjust screen resolution on non Tegra device"
+      return False
+
+    results = self.getInfo('screen')
+    parts = results['screen'][0].split(':')
+    if (self.debug >= 3): print "INFO: we have a current resolution of %s, %s" % (parts[1].split()[0], parts[2].split()[0])
+
+    #verify screen type is valid, and set it to the proper value (https://bugzilla.mozilla.org/show_bug.cgi?id=632895#c4)
+    screentype = -1
+    if (type == 'hdmi'):
+      screentype = 5
+    elif (type == 'vga' or type == 'crt'):
+      screentype = 3
+    else:
+      return False
+
+    #verify we have numbers
+    if not (isinstance(width, int) and isinstance(height, int)):
+      return False
+
+    if (width < 100 or width > 9999):
+      return False
+
+    if (height < 100 or height > 9999):
+      return False
+
+    if (self.debug >= 3): print "INFO: adjusting screen resolution to %s, %s and rebooting" % (width, height)
+    try:
+      self.verifySendCMD(["exec setprop persist.tegra.dpy%s.mode.width %s" % (screentype, width)])
+      self.verifySendCMD(["exec setprop persist.tegra.dpy%s.mode.height %s" % (screentype, height)])
+    except(DMError):
+      return False
+
+    return True
+
+gCallbackData = ''
+
+class myServer(SocketServer.TCPServer):
+  allow_reuse_address = True
+
+class callbackServer():
+  def __init__(self, ip, port, debuglevel):
+    global gCallbackData
+    if (debuglevel >= 1): print "DEBUG: gCallbackData is: %s on port: %s" % (gCallbackData, port)
+    gCallbackData = ''
+    self.ip = ip
+    self.port = port
+    self.connected = False
+    self.debug = debuglevel
+    if (self.debug >= 3): print "Creating server with " + str(ip) + ":" + str(port)
+    self.server = myServer((ip, port), self.myhandler)
+    self.server_thread = Thread(target=self.server.serve_forever) 
+    self.server_thread.setDaemon(True)
+    self.server_thread.start()
+
+  def disconnect(self, step = 60, timeout = 600):
+    t = 0
+    if (self.debug >= 3): print "Calling disconnect on callback server"
+    while t < timeout:
+      if (gCallbackData):
+        # Got the data back
+        if (self.debug >= 3): print "Got data back from agent: " + str(gCallbackData)
+        break
+      else:
+        if (self.debug >= 0): print '.',
+      time.sleep(step)
+      t += step
+
+    try:
+      if (self.debug >= 3): print "Shutting down server now"
+      self.server.shutdown()
+    except:
+      if (self.debug >= 1): print "Unable to shutdown callback server - check for a connection on port: " + str(self.port)
+
+    #sleep 1 additional step to ensure not only we are online, but all our services are online
+    time.sleep(step)
+    return gCallbackData
+
+  class myhandler(SocketServer.BaseRequestHandler):
+    def handle(self):
+      global gCallbackData
+      gCallbackData = self.request.recv(1024)
+      #print "Callback Handler got data: " + str(gCallbackData)
+      self.request.send("OK")
+  
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozdevice/setup.py
@@ -0,0 +1,67 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozdevice.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Will Lachance <wlachance@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import os
+from setuptools import setup, find_packages
+
+version = '0.1'
+
+# take description from README
+here = os.path.dirname(os.path.abspath(__file__))
+try:
+    description = file(os.path.join(here, 'README.md')).read()
+except (OSError, IOError):
+    description = ''
+
+setup(name='mozdevice',
+      version=version,
+      description="Mozilla-authored device management",
+      long_description=description,
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='',
+      author='Mozilla Automation and Testing Team',
+      author_email='tools@lists.mozilla.com',
+      url='http://github.com/mozilla/mozbase',
+      license='MPL',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )
deleted file mode 100644
--- a/testing/mozbase/mozhttpd/mozhttpd.py
+++ /dev/null
@@ -1,172 +0,0 @@
-#!/usr/bin/env python
-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is mozilla.org code.
-#
-# The Initial Developer of the Original Code is
-# the Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2011
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#   Joel Maher <joel.maher@gmail.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-
-import BaseHTTPServer
-import SimpleHTTPServer
-import threading
-import sys
-import os
-import urllib
-import re
-from SocketServer import ThreadingMixIn
-
-class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
-    allow_reuse_address = True
-    
-class MozRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
-    docroot = os.getcwd()
-
-    def parse_request(self):
-        retval = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self)
-        if '?' in self.path:
-            # ignore query string, otherwise SimpleHTTPRequestHandler 
-            # will treat it as PATH_INFO for `translate_path`
-            self.path = self.path.split('?', 1)[0]
-        return retval
-
-    def translate_path(self, path):
-        path = path.strip('/').split()
-        if path == ['']:
-            path = []
-        path.insert(0, self.docroot)
-        return os.path.join(*path)
-
-    # I found on my local network that calls to this were timing out
-    # I believe all of these calls are from log_message
-    def address_string(self):
-        return "a.b.c.d"
-
-    # This produces a LOT of noise
-    def log_message(self, format, *args):
-        pass
-
-class MozHttpd(object):
-
-    def __init__(self, host="127.0.0.1", port=8888, docroot=os.getcwd()):
-        self.host = host
-        self.port = int(port)
-        self.docroot = docroot
-        self.httpd = None
-
-    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()
-        """
-
-        class MozRequestHandlerInstance(MozRequestHandler):
-            docroot = self.docroot
-
-        self.httpd = EasyServer((self.host, self.port), MozRequestHandlerInstance)
-        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 testServer(self):
-        fileList = os.listdir(self.docroot)
-        filehandle = urllib.urlopen('http://%s:%s/?foo=bar&fleem=&foo=fleem' % (self.host, self.port))
-        data = filehandle.readlines()
-        filehandle.close()
-
-        retval = True
-
-        for line in data:
-            found = False
-            # '@' denotes a symlink and we need to ignore it.
-            webline = re.sub('\<[a-zA-Z0-9\-\_\.\=\"\'\/\\\%\!\@\#\$\^\&\*\(\) ]*\>', '', line.strip('\n')).strip('/').strip().strip('@')
-            if webline != "":
-                if webline == "Directory listing for":
-                    found = True
-                else:
-                    for fileName in fileList:
-                        if fileName == webline:
-                            found = True
-                
-                if not found:
-                    retval = False
-                    print >> sys.stderr, "NOT FOUND: " + webline.strip()
-        return retval
-
-    def stop(self):
-        if self.httpd:
-            self.httpd.shutdown()
-        self.httpd = None
-
-    __del__ = stop
-
-
-def main(args=sys.argv[1:]):
-    
-    # parse command line options
-    from optparse import OptionParser
-    parser = OptionParser()
-    parser.add_option('-p', '--port', dest='port', 
-                      type="int", default=8888,
-                      help="port to run the server on [DEFAULT: %default]")
-    parser.add_option('-H', '--host', dest='host',
-                      default='127.0.0.1',
-                      help="host [DEFAULT: %default]")
-    parser.add_option('-d', '--docroot', dest='docroot',
-                      default=os.getcwd(),
-                      help="directory to serve files from [DEFAULT: %default]")
-    parser.add_option('--test', dest='test',
-                      action='store_true', default=False,
-                      help='run the tests and exit')
-    options, args = parser.parse_args(args)
-    if args:
-        parser.print_help()
-        parser.exit()
-
-    # create the server
-    kwargs = options.__dict__.copy()
-    test = kwargs.pop('test')
-    server = MozHttpd(**kwargs)
-
-    if test:
-        server.start()
-        server.testServer()
-    else:
-        server.start(block=True)
-
-if __name__ == '__main__':
-    main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozhttpd/mozhttpd/__init__.py
@@ -0,0 +1,39 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   William Lachance <wlachance@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+from mozhttpd import MozHttpd, MozRequestHandler
+import iface
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozhttpd/mozhttpd/iface.py
@@ -0,0 +1,62 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Joel Maher <joel.maher@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+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():
+    ip = socket.gethostbyname(socket.gethostname())
+    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
new file mode 100755
--- /dev/null
+++ b/testing/mozbase/mozhttpd/mozhttpd/mozhttpd.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python
+
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Joel Maher <joel.maher@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+import BaseHTTPServer
+import SimpleHTTPServer
+import threading
+import sys
+import os
+import urllib
+import re
+from SocketServer import ThreadingMixIn
+
+class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
+    allow_reuse_address = True
+    
+class MozRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+    docroot = os.getcwd()
+
+    def parse_request(self):
+        retval = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self)
+        if '?' in self.path:
+            # ignore query string, otherwise SimpleHTTPRequestHandler 
+            # will treat it as PATH_INFO for `translate_path`
+            self.path = self.path.split('?', 1)[0]
+        return retval
+
+    def translate_path(self, path):
+        path = path.strip('/').split()
+        if path == ['']:
+            path = []
+        path.insert(0, self.docroot)
+        return os.path.join(*path)
+
+    # I found on my local network that calls to this were timing out
+    # I believe all of these calls are from log_message
+    def address_string(self):
+        return "a.b.c.d"
+
+    # This produces a LOT of noise
+    def log_message(self, format, *args):
+        pass
+
+class MozHttpd(object):
+
+    def __init__(self, host="127.0.0.1", port=8888, docroot=os.getcwd(), handler_class=MozRequestHandler):
+        self.host = host
+        self.port = int(port)
+        self.docroot = docroot
+        self.httpd = None
+
+        class MozRequestHandlerInstance(handler_class):
+            docroot = self.docroot
+
+        self.handler_class = MozRequestHandlerInstance
+
+    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()
+        """
+        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 testServer(self):
+        fileList = os.listdir(self.docroot)
+        filehandle = urllib.urlopen('http://%s:%s/?foo=bar&fleem=&foo=fleem' % (self.host, self.port))
+        data = filehandle.readlines()
+        filehandle.close()
+
+        retval = True
+
+        for line in data:
+            found = False
+            # '@' denotes a symlink and we need to ignore it.
+            webline = re.sub('\<[a-zA-Z0-9\-\_\.\=\"\'\/\\\%\!\@\#\$\^\&\*\(\) ]*\>', '', line.strip('\n')).strip('/').strip().strip('@')
+            if webline != "":
+                if webline == "Directory listing for":
+                    found = True
+                else:
+                    for fileName in fileList:
+                        if fileName == webline:
+                            found = True
+                
+                if not found:
+                    retval = False
+                    print >> sys.stderr, "NOT FOUND: " + webline.strip()
+        return retval
+
+    def stop(self):
+        if self.httpd:
+            self.httpd.shutdown()
+        self.httpd = None
+
+    __del__ = stop
+
+
+def main(args=sys.argv[1:]):
+    
+    # parse command line options
+    from optparse import OptionParser
+    parser = OptionParser()
+    parser.add_option('-p', '--port', dest='port', 
+                      type="int", default=8888,
+                      help="port to run the server on [DEFAULT: %default]")
+    parser.add_option('-H', '--host', dest='host',
+                      default='127.0.0.1',
+                      help="host [DEFAULT: %default]")
+    parser.add_option('-d', '--docroot', dest='docroot',
+                      default=os.getcwd(),
+                      help="directory to serve files from [DEFAULT: %default]")
+    parser.add_option('--test', dest='test',
+                      action='store_true', default=False,
+                      help='run the tests and exit')
+    options, args = parser.parse_args(args)
+    if args:
+        parser.print_help()
+        parser.exit()
+
+    # create the server
+    kwargs = options.__dict__.copy()
+    test = kwargs.pop('test')
+    server = MozHttpd(**kwargs)
+
+    if test:
+        server.start()
+        server.testServer()
+    else:
+        server.start(block=True)
+
+if __name__ == '__main__':
+    main()
--- a/testing/mozbase/mozhttpd/setup.py
+++ b/testing/mozbase/mozhttpd/setup.py
@@ -31,17 +31,17 @@
 # decision by deleting the provisions above and replace them with the notice
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 import os
-from setuptools import setup
+from setuptools import setup, find_packages
 
 try:
     here = os.path.dirname(os.path.abspath(__file__))
     description = file(os.path.join(here, 'README.md')).read()
 except IOError:
     description = None
 
 version = '0.1'
@@ -54,17 +54,17 @@ setup(name='mozhttpd',
       long_description=description,
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Joel Maher',
       author_email='tools@lists.mozilla.org',
       url='https://github.com/mozilla/mozbase/tree/master/mozhttpd',
       license='MPL',
       py_modules=['mozhttpd'],
-      packages=[],
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       mozhttpd = mozhttpd:main
       """,
deleted file mode 100644
--- a/testing/mozbase/mozinfo/mozinfo.py
+++ /dev/null
@@ -1,208 +0,0 @@
-#!/usr/bin/env python
-
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is mozinfo.
-#
-# The Initial Developer of the Original Code is
-#  The Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2010
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#  Jeff Hammel <jhammel@mozilla.com>
-#  Clint Talbert <ctalbert@mozilla.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-
-"""
-file for interface to transform introspected system information to a format
-pallatable to Mozilla
-
-Information:
-- os : what operating system ['win', 'mac', 'linux', ...]
-- bits : 32 or 64
-- processor : processor architecture ['x86', 'x86_64', 'ppc', ...]
-- version : operating system version string
-
-For windows, the service pack information is also included
-"""
-
-# TODO: it might be a good idea of adding a system name (e.g. 'Ubuntu' for
-# linux) to the information; I certainly wouldn't want anyone parsing this
-# information and having behaviour depend on it
-
-import os
-import platform
-import re
-import sys
-
-# keep a copy of the os module since updating globals overrides this
-_os = os
-
-class unknown(object):
-    """marker class for unknown information"""
-    def __nonzero__(self):
-        return False
-    def __str__(self):
-        return 'UNKNOWN'
-unknown = unknown() # singleton
-
-# get system information
-info = {'os': unknown,
-        'processor': unknown,
-        'version': unknown,
-        'bits': unknown }
-(system, node, release, version, machine, processor) = platform.uname()
-(bits, linkage) = platform.architecture()
-
-# get os information and related data
-if system in ["Microsoft", "Windows"]:
-    info['os'] = 'win'
-    # There is a Python bug on Windows to determine platform values
-    # http://bugs.python.org/issue7860
-    if "PROCESSOR_ARCHITEW6432" in os.environ:
-        processor = os.environ.get("PROCESSOR_ARCHITEW6432", processor)
-    else:
-        processor = os.environ.get('PROCESSOR_ARCHITECTURE', processor)
-        system = os.environ.get("OS", system).replace('_', ' ')
-        service_pack = os.sys.getwindowsversion()[4]
-        info['service_pack'] = service_pack
-elif system == "Linux":
-    (distro, version, codename) = platform.dist()
-    version = "%s %s" % (distro, version)
-    if not processor:
-        processor = machine
-    info['os'] = 'linux'
-elif system == "Darwin":
-    (release, versioninfo, machine) = platform.mac_ver()
-    version = "OS X %s" % release
-    info['os'] = 'mac'
-elif sys.platform in ('solaris', 'sunos5'):
-    info['os'] = 'unix'
-    version = sys.platform
-info['version'] = version # os version
-
-# processor type and bits
-if processor in ["i386", "i686"]:
-    if bits == "32bit":
-        processor = "x86"
-    elif bits == "64bit":
-        processor = "x86_64"
-elif processor == "AMD64":
-    bits = "64bit"
-    processor = "x86_64"
-elif processor == "Power Macintosh":
-    processor = "ppc"
-bits = re.search('(\d+)bit', bits).group(1)
-info.update({'processor': processor,
-             'bits': int(bits),
-            })
-
-# standard value of choices, for easy inspection
-choices = {'os': ['linux', 'win', 'mac', 'unix'],
-           'bits': [32, 64],
-           'processor': ['x86', 'x86_64', 'ppc']}
-
-
-def sanitize(info):
-    """Do some sanitization of input values, primarily
-    to handle universal Mac builds."""
-    if "processor" in info and info["processor"] == "universal-x86-x86_64":
-        # If we're running on OS X 10.6 or newer, assume 64-bit
-        if release[:4] >= "10.6": # Note this is a string comparison
-            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"""
-    info.update(new_info)
-    sanitize(info)
-    globals().update(info)
-
-    # convenience data for os access
-    for os_name in choices['os']:
-        globals()['is' + os_name.title()] = info['os'] == os_name
-    # unix is special
-    if isLinux:
-        globals()['isUnix'] = True
-
-update({})
-
-# exports
-__all__ = info.keys()
-__all__ += ['is' + os_name.title() for os_name in choices['os']]
-__all__ += ['info', 'unknown', 'main', 'choices', 'update']
-
-
-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,
-                          action='store_true', default=False,
-                          help="display choices for %s" % key)
-    options, args = parser.parse_args()
-
-    # args are JSON blobs to override info
-    if args:
-        try:
-            from json import loads
-        except ImportError:
-            try:
-                from simplejson import loads
-            except ImportError:
-                def loads(string):
-                    """*really* simple json; will not work with unicode"""
-                    return eval(string, {'true': True, 'false': False, 'null': None})
-        for arg in args:
-            if _os.path.exists(arg):
-                string = file(arg).read()
-            else:
-                string = arg
-            update(loads(string))
-
-    # print out choices if requested
-    flag = False
-    for key, value in options.__dict__.items():
-        if value is True:
-            print '%s choices: %s' % (key, ' '.join([str(choice)
-                                                     for choice in choices[key]]))
-            flag = True
-    if flag: return
-
-    # otherwise, print out all info
-    for key, value in info.items():
-        print '%s: %s' % (key, value)
-
-if __name__ == '__main__':
-    main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinfo/mozinfo/__init__.py
@@ -0,0 +1,39 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozinfo.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Jeff Hammel <jhammel@mozilla.com>
+#  Clint Talbert <ctalbert@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+from mozinfo import *
new file mode 100755
--- /dev/null
+++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python
+
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozinfo.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Jeff Hammel <jhammel@mozilla.com>
+#  Clint Talbert <ctalbert@mozilla.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+"""
+file for interface to transform introspected system information to a format
+pallatable to Mozilla
+
+Information:
+- os : what operating system ['win', 'mac', 'linux', ...]
+- bits : 32 or 64
+- processor : processor architecture ['x86', 'x86_64', 'ppc', ...]
+- version : operating system version string
+
+For windows, the service pack information is also included
+"""
+
+# TODO: it might be a good idea of adding a system name (e.g. 'Ubuntu' for
+# linux) to the information; I certainly wouldn't want anyone parsing this
+# information and having behaviour depend on it
+
+import os
+import platform
+import re
+import sys
+
+# keep a copy of the os module since updating globals overrides this
+_os = os
+
+class unknown(object):
+    """marker class for unknown information"""
+    def __nonzero__(self):
+        return False
+    def __str__(self):
+        return 'UNKNOWN'
+unknown = unknown() # singleton
+
+# get system information
+info = {'os': unknown,
+        'processor': unknown,
+        'version': unknown,
+        'bits': unknown }
+(system, node, release, version, machine, processor) = platform.uname()
+(bits, linkage) = platform.architecture()
+
+# get os information and related data
+if system in ["Microsoft", "Windows"]:
+    info['os'] = 'win'
+    # There is a Python bug on Windows to determine platform values
+    # http://bugs.python.org/issue7860
+    if "PROCESSOR_ARCHITEW6432" in os.environ:
+        processor = os.environ.get("PROCESSOR_ARCHITEW6432", processor)
+    else:
+        processor = os.environ.get('PROCESSOR_ARCHITECTURE', processor)
+        system = os.environ.get("OS", system).replace('_', ' ')
+        service_pack = os.sys.getwindowsversion()[4]
+        info['service_pack'] = service_pack
+elif system == "Linux":
+    (distro, version, codename) = platform.dist()
+    version = "%s %s" % (distro, version)
+    if not processor:
+        processor = machine
+    info['os'] = 'linux'
+elif system == "Darwin":
+    (release, versioninfo, machine) = platform.mac_ver()
+    version = "OS X %s" % release
+    info['os'] = 'mac'
+elif sys.platform in ('solaris', 'sunos5'):
+    info['os'] = 'unix'
+    version = sys.platform
+info['version'] = version # os version
+
+# processor type and bits
+if processor in ["i386", "i686"]:
+    if bits == "32bit":
+        processor = "x86"
+    elif bits == "64bit":
+        processor = "x86_64"
+elif processor == "AMD64":
+    bits = "64bit"
+    processor = "x86_64"
+elif processor == "Power Macintosh":
+    processor = "ppc"
+bits = re.search('(\d+)bit', bits).group(1)
+info.update({'processor': processor,
+             'bits': int(bits),
+            })
+
+# standard value of choices, for easy inspection
+choices = {'os': ['linux', 'win', 'mac', 'unix'],
+           'bits': [32, 64],
+           'processor': ['x86', 'x86_64', 'ppc']}
+
+
+def sanitize(info):
+    """Do some sanitization of input values, primarily
+    to handle universal Mac builds."""
+    if "processor" in info and info["processor"] == "universal-x86-x86_64":
+        # If we're running on OS X 10.6 or newer, assume 64-bit
+        if release[:4] >= "10.6": # Note this is a string comparison
+            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"""
+    info.update(new_info)
+    sanitize(info)
+    globals().update(info)
+
+    # convenience data for os access
+    for os_name in choices['os']:
+        globals()['is' + os_name.title()] = info['os'] == os_name
+    # unix is special
+    if isLinux:
+        globals()['isUnix'] = True
+
+update({})
+
+# exports
+__all__ = info.keys()
+__all__ += ['is' + os_name.title() for os_name in choices['os']]
+__all__ += ['info', 'unknown', 'main', 'choices', 'update']
+
+
+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,
+                          action='store_true', default=False,
+                          help="display choices for %s" % key)
+    options, args = parser.parse_args()
+
+    # args are JSON blobs to override info
+    if args:
+        try:
+            from json import loads
+        except ImportError:
+            try:
+                from simplejson import loads
+            except ImportError:
+                def loads(string):
+                    """*really* simple json; will not work with unicode"""
+                    return eval(string, {'true': True, 'false': False, 'null': None})
+        for arg in args:
+            if _os.path.exists(arg):
+                string = file(arg).read()
+            else:
+                string = arg
+            update(loads(string))
+
+    # print out choices if requested
+    flag = False
+    for key, value in options.__dict__.items():
+        if value is True:
+            print '%s choices: %s' % (key, ' '.join([str(choice)
+                                                     for choice in choices[key]]))
+            flag = True
+    if flag: return
+
+    # otherwise, print out all info
+    for key, value in info.items():
+        print '%s: %s' % (key, value)
+
+if __name__ == '__main__':
+    main()
--- a/testing/mozbase/mozinfo/setup.py
+++ b/testing/mozbase/mozinfo/setup.py
@@ -32,17 +32,17 @@
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 
 import os
-from setuptools import setup
+from setuptools import setup, find_packages
 
 version = '0.3.3'
 
 # 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):
@@ -60,18 +60,17 @@ setup(name='mozinfo',
       description="file for interface to transform introspected system information to a format pallatable to Mozilla",
       long_description=description,
       classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='Jeff Hammel',
       author_email='jhammel@mozilla.com',
       url='https://wiki.mozilla.org/Auto-tools',
       license='MPL',
-      py_modules=['mozinfo'],
-      packages=[],
+      packages=find_packages(exclude=['legacy']),
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       mozinfo = mozinfo:main
       """,
deleted file mode 100644
--- a/testing/mozbase/mozinstall/mozinstall.py
+++ /dev/null
@@ -1,209 +0,0 @@
-#!/usr/bin/env python
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is mozinstall.
-#
-# The Initial Developer of the Original Code is
-#  The Mozilla Foundation.
-# Portions created by the Initial Developer are Copyright (C) 2011
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#  Clint Talbert <ctalbert@mozilla.com>
-#  Andrew Halberstadt <halbersa@gmail.com>
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-
-from optparse import OptionParser
-import mozinfo
-import subprocess
-import zipfile
-import tarfile
-import sys
-import os
-
-_default_apps = ["firefox",
-                 "thunderbird",
-                 "fennec"]
-
-def install(src, dest=None, apps=_default_apps):
-    """
-    Installs a zip, exe, tar.gz, tar.bz2 or dmg file
-    src - the path to the install file
-    dest - the path to install to [default is os.path.dirname(src)]
-    returns - the full path to the binary in the installed folder
-              or None if the binary cannot be found
-    """
-    src = os.path.realpath(src)
-    assert(os.path.isfile(src))
-    if not dest:
-        dest = os.path.dirname(src)
-
-    trbk = None
-    try:
-        install_dir = None
-        if zipfile.is_zipfile(src) or tarfile.is_tarfile(src):
-            install_dir = _extract(src, dest)[0]
-        elif mozinfo.isMac and src.lower().endswith(".dmg"):
-            install_dir = _install_dmg(src, dest)
-        elif mozinfo.isWin and os.access(src, os.X_OK):
-            install_dir = _install_exe(src, dest)
-        else:
-            raise InvalidSource(src + " is not a recognized file type " +
-                                      "(zip, exe, tar.gz, tar.bz2 or dmg)")
-    except InvalidSource, e:
-        raise
-    except Exception, e:
-        cls, exc, trbk = sys.exc_info()
-        install_error = InstallError("Failed to install %s" % src)
-        raise install_error.__class__, install_error, trbk
-    finally:
-        # trbk won't get GC'ed due to circular reference
-        # http://docs.python.org/library/sys.html#sys.exc_info
-        del trbk
-
-    if install_dir:
-        return get_binary(install_dir, apps=apps)
-
-def get_binary(path, apps=_default_apps):
-    """
-    Finds the binary in the specified path
-    path - the path within which to search for the binary
-    returns - the full path to the binary in the folder
-              or None if the binary cannot be found
-    """
-    if mozinfo.isWin:
-        apps = [app + ".exe" for app in apps]
-    for root, dirs, files in os.walk(path):
-        for filename in files:
-            # os.access evaluates to False for some reason, so not using it
-            if filename in apps:
-                return os.path.realpath(os.path.join(root, filename))
-
-def _extract(path, extdir=None, delete=False):
-    """
-    Takes in a tar or zip file and extracts it to extdir
-    If extdir is not specified, extracts to os.path.dirname(path)
-    If delete is set to True, deletes the bundle at path
-    Returns the list of top level files that were extracted
-    """
-    if zipfile.is_zipfile(path):
-        bundle = zipfile.ZipFile(path)
-        namelist = bundle.namelist()
-    elif tarfile.is_tarfile(path):
-        bundle = tarfile.open(path)
-        namelist = bundle.getnames()
-    else:
-        return
-    if extdir is None:
-        extdir = os.path.dirname(path)
-    elif not os.path.exists(extdir):
-        os.makedirs(extdir)
-    bundle.extractall(path=extdir)
-    bundle.close()
-    if delete:
-        os.remove(path)
-    # namelist returns paths with forward slashes even in windows
-    top_level_files = [os.path.join(extdir, name) for name in namelist
-                             if len(name.rstrip('/').split('/')) == 1]
-    # namelist doesn't include folders in windows, append these to the list
-    if mozinfo.isWin:
-        for name in namelist:
-            root = name[:name.find('/')]
-            if root not in top_level_files:
-                top_level_files.append(root)
-    return top_level_files
-
-def _install_dmg(src, dest):
-    proc = subprocess.Popen("hdiutil attach " + src,
-                            shell=True,
-                            stdout=subprocess.PIPE)
-    try:
-        for data in proc.communicate()[0].split():
-            if data.find("/Volumes/") != -1:
-                appDir = data
-                break
-        for appFile in os.listdir(appDir):
-            if appFile.endswith(".app"):
-                 appName = appFile
-                 break
-        subprocess.call("cp -r " + os.path.join(appDir, appName) + " " + dest,
-                        shell=True)
-    finally:
-        subprocess.call("hdiutil detach " + appDir + " -quiet",
-                        shell=True)
-    return os.path.join(dest, appName)
-
-def _install_exe(src, dest):
-    # possibly gets around UAC in vista (still need to run as administrator)
-    os.environ['__compat_layer'] = "RunAsInvoker"
-    cmd = [src, "/S", "/D=" + os.path.realpath(dest)]
-    subprocess.call(cmd)
-    return dest
-
-def cli(argv=sys.argv[1:]):
-    parser = OptionParser()
-    parser.add_option("-s", "--source",
-                      dest="src",
-                      help="Path to installation file. "
-                           "Accepts: zip, exe, tar.bz2, tar.gz, and dmg")
-    parser.add_option("-d", "--destination",
-                      dest="dest",
-                      default=None,
-                      help="[optional] Directory to install application into")
-    parser.add_option("--app", dest="app",
-                      action="append",
-                      default=_default_apps,
-                      help="[optional] Application being installed. "
-                           "Should be lowercase, e.g: "
-                           "firefox, fennec, thunderbird, etc.")
-
-    (options, args) = parser.parse_args(argv)
-    if not options.src or not os.path.exists(options.src):
-        print "Error: must specify valid source"
-        return 2
-
-    # Run it
-    if os.path.isdir(options.src):
-        binary = get_binary(options.src, apps=options.app)
-    else:
-        binary = install(options.src, dest=options.dest, apps=options.app)
-    print binary
-
-class InvalidSource(Exception):
-    """
-    Thrown when the specified source is not a recognized
-    file type (zip, exe, tar.gz, tar.bz2 or dmg)
-    """
-
-class InstallError(Exception):
-    """
-    Thrown when the installation fails. Includes traceback
-    if available.
-    """
-
-if __name__ == "__main__":
-    sys.exit(cli())
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinstall/mozinstall/__init__.py
@@ -0,0 +1,38 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozinstall.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Clint Talbert <ctalbert@mozilla.com>
+#  Andrew Halberstadt <halbersa@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+from mozinstall import *
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinstall/mozinstall/mozinstall.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozinstall.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Clint Talbert <ctalbert@mozilla.com>
+#  Andrew Halberstadt <halbersa@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+from optparse import OptionParser
+import mozinfo
+import subprocess
+import zipfile
+import tarfile
+import sys
+import os
+
+_default_apps = ["firefox",
+                 "thunderbird",
+                 "fennec"]
+
+def install(src, dest=None, apps=_default_apps):
+    """
+    Installs a zip, exe, tar.gz, tar.bz2 or dmg file
+    src - the path to the install file
+    dest - the path to install to [default is os.path.dirname(src)]
+    returns - the full path to the binary in the installed folder
+              or None if the binary cannot be found
+    """
+    src = os.path.realpath(src)
+    assert(os.path.isfile(src))
+    if not dest:
+        dest = os.path.dirname(src)
+
+    trbk = None
+    try:
+        install_dir = None
+        if zipfile.is_zipfile(src) or tarfile.is_tarfile(src):
+            install_dir = _extract(src, dest)[0]
+        elif mozinfo.isMac and src.lower().endswith(".dmg"):
+            install_dir = _install_dmg(src, dest)
+        elif mozinfo.isWin and os.access(src, os.X_OK):
+            install_dir = _install_exe(src, dest)
+        else:
+            raise InvalidSource(src + " is not a recognized file type " +
+                                      "(zip, exe, tar.gz, tar.bz2 or dmg)")
+    except InvalidSource, e:
+        raise
+    except Exception, e:
+        cls, exc, trbk = sys.exc_info()
+        install_error = InstallError("Failed to install %s" % src)
+        raise install_error.__class__, install_error, trbk
+    finally:
+        # trbk won't get GC'ed due to circular reference
+        # http://docs.python.org/library/sys.html#sys.exc_info
+        del trbk
+
+    if install_dir:
+        return get_binary(install_dir, apps=apps)
+
+def get_binary(path, apps=_default_apps):
+    """
+    Finds the binary in the specified path
+    path - the path within which to search for the binary
+    returns - the full path to the binary in the folder
+              or None if the binary cannot be found
+    """
+    if mozinfo.isWin:
+        apps = [app + ".exe" for app in apps]
+    for root, dirs, files in os.walk(path):
+        for filename in files:
+            # os.access evaluates to False for some reason, so not using it
+            if filename in apps:
+                return os.path.realpath(os.path.join(root, filename))
+
+def _extract(path, extdir=None, delete=False):
+    """
+    Takes in a tar or zip file and extracts it to extdir
+    If extdir is not specified, extracts to os.path.dirname(path)
+    If delete is set to True, deletes the bundle at path
+    Returns the list of top level files that were extracted
+    """
+    if zipfile.is_zipfile(path):
+        bundle = zipfile.ZipFile(path)
+        namelist = bundle.namelist()
+    elif tarfile.is_tarfile(path):
+        bundle = tarfile.open(path)
+        namelist = bundle.getnames()
+    else:
+        return
+    if extdir is None:
+        extdir = os.path.dirname(path)
+    elif not os.path.exists(extdir):
+        os.makedirs(extdir)
+    bundle.extractall(path=extdir)
+    bundle.close()
+    if delete:
+        os.remove(path)
+    # namelist returns paths with forward slashes even in windows
+    top_level_files = [os.path.join(extdir, name) for name in namelist
+                             if len(name.rstrip('/').split('/')) == 1]
+    # namelist doesn't include folders in windows, append these to the list
+    if mozinfo.isWin:
+        for name in namelist:
+            root = name[:name.find('/')]
+            if root not in top_level_files:
+                top_level_files.append(root)
+    return top_level_files
+
+def _install_dmg(src, dest):
+    proc = subprocess.Popen("hdiutil attach " + src,
+                            shell=True,
+                            stdout=subprocess.PIPE)
+    try:
+        for data in proc.communicate()[0].split():
+            if data.find("/Volumes/") != -1:
+                appDir = data
+                break
+        for appFile in os.listdir(appDir):
+            if appFile.endswith(".app"):
+                 appName = appFile
+                 break
+        subprocess.call("cp -r " + os.path.join(appDir, appName) + " " + dest,
+                        shell=True)
+    finally:
+        subprocess.call("hdiutil detach " + appDir + " -quiet",
+                        shell=True)
+    return os.path.join(dest, appName)
+
+def _install_exe(src, dest):
+    # possibly gets around UAC in vista (still need to run as administrator)
+    os.environ['__compat_layer'] = "RunAsInvoker"
+    cmd = [src, "/S", "/D=" + os.path.realpath(dest)]
+    subprocess.call(cmd)
+    return dest
+
+def cli(argv=sys.argv[1:]):
+    parser = OptionParser()
+    parser.add_option("-s", "--source",
+                      dest="src",
+                      help="Path to installation file. "
+                           "Accepts: zip, exe, tar.bz2, tar.gz, and dmg")
+    parser.add_option("-d", "--destination",
+                      dest="dest",
+                      default=None,
+                      help="[optional] Directory to install application into")
+    parser.add_option("--app", dest="app",
+                      action="append",
+                      default=_default_apps,
+                      help="[optional] Application being installed. "
+                           "Should be lowercase, e.g: "
+                           "firefox, fennec, thunderbird, etc.")
+
+    (options, args) = parser.parse_args(argv)
+    if not options.src or not os.path.exists(options.src):
+        print "Error: must specify valid source"
+        return 2
+
+    # Run it
+    if os.path.isdir(options.src):
+        binary = get_binary(options.src, apps=options.app)
+    else:
+        binary = install(options.src, dest=options.dest, apps=options.app)
+    print binary
+
+class InvalidSource(Exception):
+    """
+    Thrown when the specified source is not a recognized
+    file type (zip, exe, tar.gz, tar.bz2 or dmg)
+    """
+
+class InstallError(Exception):
+    """
+    Thrown when the installation fails. Includes traceback
+    if available.
+    """
+
+if __name__ == "__main__":
+    sys.exit(cli())
--- a/testing/mozbase/mozinstall/setup.py
+++ b/testing/mozbase/mozinstall/setup.py
@@ -32,17 +32,17 @@
 # decision by deleting the provisions above and replace them with the notice
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 import os
-from setuptools import setup
+from setuptools import setup, find_packages
 
 try:
     here = os.path.dirname(os.path.abspath(__file__))
     description = file(os.path.join(here, 'README.md')).read()
 except IOError:
     description = None
 
 version = '0.3'
@@ -61,18 +61,17 @@ setup(name='mozInstall',
                    'Programming Language :: Python',
                    'Topic :: Software Development :: Libraries :: Python Modules',
                   ], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
       keywords='mozilla',
       author='mdas',
       author_email='mdas@mozilla.com',
       url='https://github.com/mozilla/mozbase',
       license='MPL',
-      py_modules=['mozinstall'],
-      packages=[],
+      packages=find_packages(exclude=['legacy']),
       include_package_data=True,
       zip_safe=False,
       install_requires=deps,
       entry_points="""
       # -*- Entry points: -*-
       [console_scripts]
       mozinstall = mozinstall:cli
       """,
old mode 100644
new mode 100755
--- a/testing/mozbase/mozprofile/setup.py
+++ b/testing/mozbase/mozprofile/setup.py
@@ -37,22 +37,22 @@
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
 import os
 import sys
 from setuptools import setup, find_packages
 
-version = '0.1b2'
+version = '0.1'
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
-deps = ["ManifestDestiny == 0.5.4"]
+deps = ["ManifestDestiny >= 0.5.4"]
 # version-dependent dependencies
 try:
     import json
 except ImportError:
     deps.append('simplejson')
 
 # take description from README
 here = os.path.dirname(os.path.abspath(__file__))
--- a/testing/mozbase/mozrunner/mozrunner/utils.py
+++ b/testing/mozbase/mozrunner/mozrunner/utils.py
@@ -51,17 +51,20 @@ import os
 import sys
 
 
 ### python package method metadata by introspection
 try:
     import pkg_resources
     def get_metadata_from_egg(module):
         ret = {}
-        dist = pkg_resources.get_distribution(module)
+        try:
+            dist = pkg_resources.get_distribution(module)
+        except pkg_resources.DistributionNotFound:
+            return {}
         if dist.has_metadata("PKG-INFO"):
             key = None
             for line in dist.get_metadata("PKG-INFO").splitlines():
                 # see http://www.python.org/dev/peps/pep-0314/
                 if key == 'Description':
                     # descriptions can be long
                     if not line or line[0].isspace():
                         value += '\n' + line
--- a/testing/mozbase/mozrunner/setup.py
+++ b/testing/mozbase/mozrunner/setup.py
@@ -38,27 +38,30 @@
 #
 # ***** END LICENSE BLOCK *****
 
 import os
 import sys
 from setuptools import setup, find_packages
 
 PACKAGE_NAME = "mozrunner"
-PACKAGE_VERSION = "4.0"
+PACKAGE_VERSION = "4.1"
 
 desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
 # take description from README
 here = os.path.dirname(os.path.abspath(__file__))
 try:
     description = file(os.path.join(here, 'README.md')).read()
 except (OSError, IOError):
     description = ''
 
-deps = ['mozprocess', 'mozprofile', 'mozinfo']
+deps = ['mozinfo',
+        'mozprocess',
+        'mozprofile >= 0.1',
+       ]
 
 # we only support python 2 right now
 assert sys.version_info[0] == 2
 
 setup(name=PACKAGE_NAME,
       version=PACKAGE_VERSION,
       description=desc,
       long_description=description,
old mode 100644
new mode 100755
--- a/testing/peptest/Makefile.in
+++ b/testing/peptest/Makefile.in
@@ -47,22 +47,31 @@ MODULE = testing_peptest
 include $(topsrcdir)/config/rules.mk
 
 PEPTEST_HARNESS = \
   peptest \
   $(NULL)
 
 PEPTEST_EXTRAS = \
   setup.py \
+  runtests.py \
   MANIFEST.in \
   README.md \
   $(NULL)
 
 PEPTEST_TESTS = \
   tests \
   $(NULL)
 
+_DEST_DIR = $(DEPTH)/_tests/peptest
+libs:: $(PEPTEST_HARNESS)
+	$(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR)
+libs:: $(PEPTEST_EXTRAS)
+	$(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR)
+libs:: $(PEPTEST_TESTS)
+	$(PYTHON) $(topsrcdir)/config/nsinstall.py $^ $(_DEST_DIR)
+
 stage-package: PKG_STAGE = $(DIST)/test-package-stage
 stage-package:
 	$(NSINSTALL) -D $(PKG_STAGE)/peptest
 	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_HARNESS)) | (cd $(PKG_STAGE)/peptest && tar -xf -)
 	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_EXTRAS)) | (cd $(PKG_STAGE)/peptest && tar -xf -)
 	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(PEPTEST_TESTS)) | (cd $(PKG_STAGE)/peptest && tar -xf -)
new file mode 100644
--- /dev/null
+++ b/testing/peptest/runtests.py
@@ -0,0 +1,61 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is peptest.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Andrew Halberstadt <halbersa@gmail.com>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+"""
+Adds peptest's dependencies to sys.path then runs the tests
+"""
+import os
+import sys
+
+deps = ['manifestdestiny',
+        'mozinfo',
+        'mozhttpd',
+        'mozlog',
+        'mozprofile',
+        'mozprocess',
+        'mozrunner',
+       ]
+
+here = os.path.dirname(__file__)
+mozbase = os.path.realpath(os.path.join(here, '..', 'mozbase'))
+
+for dep in deps:
+    module = os.path.join(mozbase, dep)
+    if module not in sys.path:
+        sys.path.insert(0, module)
+
+from peptest import runpeptests
+runpeptests.main(sys.argv[1:])
--- a/testing/testsuite-targets.mk
+++ b/testing/testsuite-targets.mk
@@ -36,18 +36,20 @@
 #
 # ***** END LICENSE BLOCK *****
 
 
 # Shortcut for mochitest* and xpcshell-tests targets,
 # replaces 'EXTRA_TEST_ARGS=--test-path=...'.
 ifdef TEST_PATH
 TEST_PATH_ARG := --test-path=$(TEST_PATH)
+PEPTEST_PATH_ARG := --test-path=$(TEST_PATH)
 else
 TEST_PATH_ARG :=
+PEPTEST_PATH_ARG := --test-path=_tests/peptest/tests/firefox/firefox_all.ini
 endif
 
 # include automation-build.mk to get the path to the binary
 TARGET_DEPTH = $(DEPTH)
 include $(topsrcdir)/build/binary-location.mk
 
 SYMBOLS_PATH := --symbols-path=$(DIST)/crashreporter-symbols
 
@@ -223,16 +225,26 @@ REMOTE_XPCSHELL = \
 xpcshell-tests-remote: DM_TRANS?=adb
 xpcshell-tests-remote:
 	@if [ "${TEST_DEVICE}" != "" -o "$(DM_TRANS)" = "adb" ]; \
           then $(call REMOTE_XPCSHELL); $(CHECK_TEST_ERROR); \
         else \
           echo "please prepare your host with environment variables for TEST_DEVICE"; \
         fi
 
+# Runs peptest, for usage see: https://developer.mozilla.org/en/Peptest#Running_Tests
+RUN_PEPTEST = \
+	rm -f ./$@.log && \
+	$(PYTHON) _tests/peptest/runtests.py --binary=$(browser_path) $(PEPTEST_PATH_ARG) \
+	  --log-file=./$@.log $(SYMBOLS_PATH) $(EXTRA_TEST_ARGS)
+
+peptest:
+	$(RUN_PEPTEST)
+	$(CHECK_TEST_ERROR)
+
 # Package up the tests and test harnesses
 include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk
 
 ifndef UNIVERSAL_BINARY
 PKG_STAGE = $(DIST)/test-package-stage
 package-tests: stage-mochitest stage-reftest stage-xpcshell stage-jstests stage-jetpack stage-firebug stage-peptest stage-mozbase
 else
 # This staging area has been built for us by universal/flight.mk
@@ -286,9 +298,10 @@ stage-peptest: make-stage-dir
 
 stage-mozbase: make-stage-dir
 	$(MAKE) -C $(DEPTH)/testing/mozbase stage-package
 .PHONY: \
   mochitest mochitest-plain mochitest-chrome mochitest-a11y mochitest-ipcplugins \
   reftest crashtest \
   xpcshell-tests \
   jstestbrowser \
+  peptest \
   package-tests make-stage-dir stage-mochitest stage-reftest stage-xpcshell stage-jstests stage-android stage-jetpack stage-firebug stage-peptest stage-mozbase