Bug 706844 - Create a make target for peptest. r=jmaher
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Tue, 06 Dec 2011 09:26:24 -0500
changeset 82870 65c05ff60e47d68eebe82705b389c07ece2bdfcd
parent 82869 9a59028a35108dc809341ed9ecf9c71ba6e1538b
child 82871 3204b70435fe8a83ff33cbaef12e33ab3ebe3b2b
push idunknown
push userunknown
push dateunknown
reviewersjmaher
bugs706844
milestone11.0a1
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