Bug 703266 Mirror mozbase to mozilla-central for peptest r=jhammel
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Tue, 29 Nov 2011 11:43:16 -0500
changeset 80963 b10b930500f1df703e4c1b11a1ec395a9c4c89b9
parent 80962 604476c594958311b57e6cb0a78fb2d350d74eb2
child 80964 21ecdc2d0a6f929160ad463483c9189694f2d936
push id21545
push usermak77@bonardo.net
push dateWed, 30 Nov 2011 11:46:58 +0000
treeherdermozilla-central@639fd053363e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjhammel
bugs703266
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 703266 Mirror mozbase to mozilla-central for peptest r=jhammel
testing/mozbase/Makefile.in
testing/mozbase/README
testing/mozbase/manifestdestiny/README.txt
testing/mozbase/manifestdestiny/manifestparser.py
testing/mozbase/manifestdestiny/setup.py
testing/mozbase/manifestdestiny/tests/filter-example.ini
testing/mozbase/manifestdestiny/tests/fleem
testing/mozbase/manifestdestiny/tests/include-example.ini
testing/mozbase/manifestdestiny/tests/include/bar.ini
testing/mozbase/manifestdestiny/tests/include/crash-handling
testing/mozbase/manifestdestiny/tests/include/flowers
testing/mozbase/manifestdestiny/tests/include/foo.ini
testing/mozbase/manifestdestiny/tests/mozmill-example.ini
testing/mozbase/manifestdestiny/tests/mozmill-restart-example.ini
testing/mozbase/manifestdestiny/tests/path-example.ini
testing/mozbase/manifestdestiny/tests/test.py
testing/mozbase/manifestdestiny/tests/test_expressionparser.txt
testing/mozbase/manifestdestiny/tests/test_manifestparser.txt
testing/mozbase/manifestdestiny/tests/test_testmanifest.txt
testing/mozbase/mozhttpd/README.md
testing/mozbase/mozhttpd/mozhttpd.py
testing/mozbase/mozhttpd/setup.py
testing/mozbase/mozinfo/README.md
testing/mozbase/mozinfo/mozinfo.py
testing/mozbase/mozinfo/setup.py
testing/mozbase/mozinstall/README.md
testing/mozbase/mozinstall/mozinstall.py
testing/mozbase/mozinstall/setup.py
testing/mozbase/mozlog/README.md
testing/mozbase/mozlog/mozlog/__init__.py
testing/mozbase/mozlog/mozlog/logger.py
testing/mozbase/mozlog/setup.py
testing/mozbase/mozprocess/README.md
testing/mozbase/mozprocess/mozprocess/__init__.py
testing/mozbase/mozprocess/mozprocess/pid.py
testing/mozbase/mozprocess/mozprocess/processhandler.py
testing/mozbase/mozprocess/mozprocess/qijo.py
testing/mozbase/mozprocess/mozprocess/winprocess.py
testing/mozbase/mozprocess/mozprocess/wpk.py
testing/mozbase/mozprocess/setup.py
testing/mozbase/mozprofile/README.md
testing/mozbase/mozprofile/mozprofile/__init__.py
testing/mozbase/mozprofile/mozprofile/addons.py
testing/mozbase/mozprofile/mozprofile/cli.py
testing/mozbase/mozprofile/mozprofile/permissions.py
testing/mozbase/mozprofile/mozprofile/prefs.py
testing/mozbase/mozprofile/mozprofile/profile.py
testing/mozbase/mozprofile/setup.py
testing/mozbase/mozrunner/README.md
testing/mozbase/mozrunner/mozrunner/__init__.py
testing/mozbase/mozrunner/mozrunner/runner.py
testing/mozbase/mozrunner/mozrunner/utils.py
testing/mozbase/mozrunner/setup.py
testing/mozbase/setup_development.py
testing/testsuite-targets.mk
toolkit/toolkit-makefiles.sh
toolkit/toolkit-tiers.mk
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/Makefile.in
@@ -0,0 +1,73 @@
+# ***** 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):
+#   Andrew Halberstadt <halbersa@gmail.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 *****
+
+DEPTH = ../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = testing_mozbase
+
+include $(topsrcdir)/config/rules.mk
+
+# Harness packages from the srcdir;
+# python packages to be installed IN INSTALLATION ORDER.
+# Packages later in the list can depend only on packages earlier in the list.
+MOZBASE_PACKAGES = \
+  manifestdestiny \
+  mozdevice \
+  mozhttpd \
+  mozinfo \
+  mozinstall \
+  mozlog \
+  mozprocess \
+  mozprofile \
+  mozrunner \
+  $(NULL)
+
+MOZBASE_EXTRAS = \
+  setup_development.py \
+  README \
+  $(NULL)
+
+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/README
@@ -0,0 +1,7 @@
+This is the git repo for the mozbase suite of python utilities.
+
+Learn more about mozbase here: https://wiki.mozilla.org/Auto-tools/Projects/MozBase
+
+Bugs live at https://bugzilla.mozilla.org/buglist.cgi?resolution=---&component=Mozbase&product=Testing and https://bugzilla.mozilla.org/buglist.cgi?resolution=---&status_whiteboard_type=allwordssubstr&query_format=advanced&status_whiteboard=mozbase
+
+To file a bug, go to https://bugzilla.mozilla.org/enter_bug.cgi?product=Testing&component=Mozbase
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/README.txt
@@ -0,0 +1,345 @@
+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
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/manifestparser.py
@@ -0,0 +1,1114 @@
+#!/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/setup.py
@@ -0,0 +1,46 @@
+#!/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) 2011
+# 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 *****
+
+# 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
+
+import sys
+from manifestparser import SetupCLI
+SetupCLI(None)(None, sys.argv[1:])
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/filter-example.ini
@@ -0,0 +1,11 @@
+# illustrate test filters based on various categories
+
+[windowstest]
+run-if = os == 'win'
+
+[fleem]
+skip-if = os == 'mac'
+
+[linuxtest]
+skip-if = (os == 'mac') || (os == 'win')
+fail-if = toolkit == 'cocoa'
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/fleem
@@ -0,0 +1,1 @@
+# dummy spot for "fleem" test
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/include-example.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+foo = bar
+
+[include:include/bar.ini]
+
+[fleem]
+
+[include:include/foo.ini]
+red = roses
+blue = violets
+yellow = daffodils
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/include/bar.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+foo = fleem
+
+[crash-handling]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/include/crash-handling
@@ -0,0 +1,1 @@
+# dummy spot for "crash-handling" test
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/include/flowers
@@ -0,0 +1,1 @@
+# dummy spot for "flowers" test
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/include/foo.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+blue = ocean
+
+[flowers]
+yellow = submarine
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/mozmill-example.ini
@@ -0,0 +1,80 @@
+[testAddons/testDisableEnablePlugin.js]
+[testAddons/testGetAddons.js]
+[testAddons/testSearchAddons.js]
+[testAwesomeBar/testAccessLocationBar.js]
+[testAwesomeBar/testCheckItemHighlight.js]
+[testAwesomeBar/testEscapeAutocomplete.js]
+[testAwesomeBar/testFaviconInAutocomplete.js]
+[testAwesomeBar/testGoButton.js]
+[testAwesomeBar/testLocationBarSearches.js]
+[testAwesomeBar/testPasteLocationBar.js]
+[testAwesomeBar/testSuggestHistoryBookmarks.js]
+[testAwesomeBar/testVisibleItemsMax.js]
+[testBookmarks/testAddBookmarkToMenu.js]
+[testCookies/testDisableCookies.js]
+[testCookies/testEnableCookies.js]
+[testCookies/testRemoveAllCookies.js]
+[testCookies/testRemoveCookie.js]
+[testDownloading/testCloseDownloadManager.js]
+[testDownloading/testDownloadStates.js]
+[testDownloading/testOpenDownloadManager.js]
+[testFindInPage/testFindInPage.js]
+[testFormManager/testAutoCompleteOff.js]
+[testFormManager/testBasicFormCompletion.js]
+[testFormManager/testClearFormHistory.js]
+[testFormManager/testDisableFormManager.js]
+[testGeneral/testGoogleSuggestions.js]
+[testGeneral/testStopReloadButtons.js]
+[testInstallation/testBreakpadInstalled.js]
+[testLayout/testNavigateFTP.js]
+[testPasswordManager/testPasswordNotSaved.js]
+[testPasswordManager/testPasswordSavedAndDeleted.js]
+[testPopups/testPopupsAllowed.js]
+[testPopups/testPopupsBlocked.js]
+[testPreferences/testPaneRetention.js]
+[testPreferences/testPreferredLanguage.js]
+[testPreferences/testRestoreHomepageToDefault.js]
+[testPreferences/testSetToCurrentPage.js]
+[testPreferences/testSwitchPanes.js]
+[testPrivateBrowsing/testAboutPrivateBrowsing.js]
+[testPrivateBrowsing/testCloseWindow.js]
+[testPrivateBrowsing/testDisabledElements.js]
+[testPrivateBrowsing/testDisabledPermissions.js]
+[testPrivateBrowsing/testDownloadManagerClosed.js]
+[testPrivateBrowsing/testGeolocation.js]
+[testPrivateBrowsing/testStartStopPBMode.js]
+[testPrivateBrowsing/testTabRestoration.js]
+[testPrivateBrowsing/testTabsDismissedOnStop.js]
+[testSearch/testAddMozSearchProvider.js]
+[testSearch/testFocusAndSearch.js]
+[testSearch/testGetMoreSearchEngines.js]
+[testSearch/testOpenSearchAutodiscovery.js]
+[testSearch/testRemoveSearchEngine.js]
+[testSearch/testReorderSearchEngines.js]
+[testSearch/testRestoreDefaults.js]
+[testSearch/testSearchSelection.js]
+[testSearch/testSearchSuggestions.js]
+[testSecurity/testBlueLarry.js]
+[testSecurity/testDefaultPhishingEnabled.js]
+[testSecurity/testDefaultSecurityPrefs.js]
+[testSecurity/testEncryptedPageWarning.js]
+[testSecurity/testGreenLarry.js]
+[testSecurity/testGreyLarry.js]
+[testSecurity/testIdentityPopupOpenClose.js]
+[testSecurity/testSSLDisabledErrorPage.js]
+[testSecurity/testSafeBrowsingNotificationBar.js]
+[testSecurity/testSafeBrowsingWarningPages.js]
+[testSecurity/testSecurityInfoViaMoreInformation.js]
+[testSecurity/testSecurityNotification.js]
+[testSecurity/testSubmitUnencryptedInfoWarning.js]
+[testSecurity/testUnknownIssuer.js]
+[testSecurity/testUntrustedConnectionErrorPage.js]
+[testSessionStore/testUndoTabFromContextMenu.js]
+[testTabbedBrowsing/testBackgroundTabScrolling.js]
+[testTabbedBrowsing/testCloseTab.js]
+[testTabbedBrowsing/testNewTab.js]
+[testTabbedBrowsing/testNewWindow.js]
+[testTabbedBrowsing/testOpenInBackground.js]
+[testTabbedBrowsing/testOpenInForeground.js]
+[testTechnicalTools/testAccessPageInfoDialog.js]
+[testToolbar/testBackForwardButtons.js]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/mozmill-restart-example.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+type = restart
+
+[restartTests/testExtensionInstallUninstall/test2.js]
+foo = bar
+
+[restartTests/testExtensionInstallUninstall/test1.js]
+foo = baz
+
+[restartTests/testExtensionInstallUninstall/test3.js]
+[restartTests/testSoftwareUpdateAutoProxy/test2.js]
+[restartTests/testSoftwareUpdateAutoProxy/test1.js]
+[restartTests/testMasterPassword/test1.js]
+[restartTests/testExtensionInstallGetAddons/test2.js]
+[restartTests/testExtensionInstallGetAddons/test1.js]
+[restartTests/testMultipleExtensionInstallation/test2.js]
+[restartTests/testMultipleExtensionInstallation/test1.js]
+[restartTests/testThemeInstallUninstall/test2.js]
+[restartTests/testThemeInstallUninstall/test1.js]
+[restartTests/testThemeInstallUninstall/test3.js]
+[restartTests/testDefaultBookmarks/test1.js]
+[softwareUpdate/testFallbackUpdate/test2.js]
+[softwareUpdate/testFallbackUpdate/test1.js]
+[softwareUpdate/testFallbackUpdate/test3.js]
+[softwareUpdate/testDirectUpdate/test2.js]
+[softwareUpdate/testDirectUpdate/test1.js]
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/path-example.ini
@@ -0,0 +1,2 @@
+[foo]
+path = fleem
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/test.py
@@ -0,0 +1,114 @@
+#!/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
+# Mozilla.org.
+# 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 *****
+
+"""tests for ManifestDestiny"""
+
+import doctest
+import os
+import sys
+from optparse import OptionParser
+
+def run_tests(raise_on_error=False, report_first=False):
+
+    # add results here
+    results = {}
+
+    # doctest arguments
+    directory = os.path.dirname(os.path.abspath(__file__))
+    extraglobs = {}
+    doctest_args = dict(extraglobs=extraglobs,
+                        module_relative=False,
+                        raise_on_error=raise_on_error)
+    if report_first:
+        doctest_args['optionflags'] = doctest.REPORT_ONLY_FIRST_FAILURE
+                                
+    # gather tests
+    directory = os.path.dirname(os.path.abspath(__file__))
+    tests =  [ test for test in os.listdir(directory)
+               if test.endswith('.txt') and test.startswith('test_')]
+    os.chdir(directory)
+
+    # run the tests
+    for test in tests:
+        try:
+            results[test] = doctest.testfile(test, **doctest_args)
+        except doctest.DocTestFailure, failure:
+            raise
+        except doctest.UnexpectedException, failure:
+            raise failure.exc_info[0], failure.exc_info[1], failure.exc_info[2]
+        
+    return results
+                                
+
+def main(args=sys.argv[1:]):
+
+    # parse command line options
+    parser = OptionParser(description=__doc__)
+    parser.add_option('--raise', dest='raise_on_error',
+                      default=False, action='store_true',
+                      help="raise on first error")
+    parser.add_option('--report-first', dest='report_first',
+                      default=False, action='store_true',
+                      help="report the first error only (all tests will still run)")
+    parser.add_option('-q', '--quiet', dest='quiet',
+                      default=False, action='store_true',
+                      help="minimize output")
+    options, args = parser.parse_args(args)
+    quiet = options.__dict__.pop('quiet')
+
+    # run the tests
+    results = run_tests(**options.__dict__)
+
+    # check for failure
+    failed = False
+    for result in results.values():
+        if result[0]: # failure count; http://docs.python.org/library/doctest.html#basic-api
+            failed = True
+            break
+    if failed:
+        sys.exit(1) # error
+    if not quiet:
+        # print results
+        print "manifestparser.py: All tests pass!"
+        for test in sorted(results.keys()):
+            result = results[test]
+            print "%s: failed=%s, attempted=%s" % (test, result[0], result[1])
+               
+if __name__ == '__main__':
+    main()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/test_expressionparser.txt
@@ -0,0 +1,120 @@
+Test Expressionparser
+=====================
+
+Test the conditional expression parser.
+
+Boilerplate::
+
+    >>> from manifestparser import parse
+
+Test basic values::
+
+    >>> parse("1")
+    1
+    >>> parse("100")
+    100
+    >>> parse("true")
+    True
+    >>> parse("false")
+    False
+    >>> '' == parse('""')
+    True
+    >>> parse('"foo bar"')
+    'foo bar'
+    >>> parse("'foo bar'")
+    'foo bar'
+    >>> parse("foo", foo=1)
+    1
+    >>> parse("bar", bar=True)
+    True
+    >>> parse("abc123", abc123="xyz")
+    'xyz'
+
+Test equality::
+
+    >>> parse("true == true")
+    True
+    >>> parse("false == false")
+    True
+    >>> parse("false == false")
+    True
+    >>> parse("1 == 1")
+    True
+    >>> parse("100 == 100")
+    True
+    >>> parse('"some text" == "some text"')
+    True
+    >>> parse("true != false")
+    True
+    >>> parse("1 != 2")
+    True
+    >>> parse('"text" != "other text"')
+    True
+    >>> parse("foo == true", foo=True)
+    True
+    >>> parse("foo == 1", foo=1)
+    True
+    >>> parse('foo == "bar"', foo='bar')
+    True
+    >>> parse("foo == bar", foo=True, bar=True)
+    True
+    >>> parse("true == foo", foo=True)
+    True
+    >>> parse("foo != true", foo=False)
+    True
+    >>> parse("foo != 2", foo=1)
+    True
+    >>> parse('foo != "bar"', foo='abc')
+    True
+    >>> parse("foo != bar", foo=True, bar=False)
+    True
+    >>> parse("true != foo", foo=False)
+    True
+    >>> parse("!false")
+    True
+
+Test conjunctions::
+    
+    >>> parse("true && true")
+    True
+    >>> parse("true || false")
+    True
+    >>> parse("false || false")
+    False
+    >>> parse("true && false")
+    False
+    >>> parse("true || false && false")
+    True
+
+Test parentheses::
+    
+    >>> parse("(true)")
+    True
+    >>> parse("(10)")
+    10
+    >>> parse('("foo")')
+    'foo'
+    >>> parse("(foo)", foo=1)
+    1
+    >>> parse("(true == true)")
+    True
+    >>> parse("(true != false)")
+    True
+    >>> parse("(true && true)")
+    True
+    >>> parse("(true || false)")
+    True
+    >>> parse("(true && true || false)")
+    True
+    >>> parse("(true || false) && false")
+    False
+    >>> parse("(true || false) && true")
+    True
+    >>> parse("true && (true || false)")
+    True
+    >>> parse("true && (true || false)")
+    True
+    >>> parse("(true && false) || (true && (true || false))")
+    True
+        
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/test_manifestparser.txt
@@ -0,0 +1,217 @@
+Test the manifest parser
+========================
+
+You must have ManifestDestiny installed before running these tests.
+Run ``python manifestparser.py setup develop`` with setuptools installed.
+
+Ensure basic parser is sane::
+
+    >>> from manifestparser import ManifestParser
+    >>> parser = ManifestParser()
+    >>> parser.read('mozmill-example.ini')
+    >>> tests = parser.tests
+    >>> len(tests) == len(file('mozmill-example.ini').read().strip().splitlines())
+    True
+    
+Ensure that capitalization and order aren't an issue:
+
+    >>> lines = ['[%s]' % test['name'] for test in tests]
+    >>> lines == file('mozmill-example.ini').read().strip().splitlines()
+    True
+
+Show how you select subsets of tests:
+
+    >>> parser.read('mozmill-restart-example.ini')
+    >>> restart_tests = parser.get(type='restart')
+    >>> len(restart_tests) < len(parser.tests)
+    True
+    >>> import os
+    >>> len(restart_tests) == len(parser.get(manifest=os.path.abspath('mozmill-restart-example.ini')))
+    True
+    >>> assert not [test for test in restart_tests if test['manifest'] != os.path.abspath('mozmill-restart-example.ini')]
+    >>> parser.get('name', tags=['foo'])
+    ['restartTests/testExtensionInstallUninstall/test2.js', 'restartTests/testExtensionInstallUninstall/test1.js']
+    >>> parser.get('name', foo='bar')
+    ['restartTests/testExtensionInstallUninstall/test2.js']
+
+Illustrate how include works::
+
+    >>> parser = ManifestParser(manifests=('include-example.ini',))
+
+All of the tests should be included, in order::
+
+    >>> parser.get('name')
+    ['crash-handling', 'fleem', 'flowers']
+    >>> [(test['name'], os.path.basename(test['manifest'])) for test in parser.tests]
+    [('crash-handling', 'bar.ini'), ('fleem', 'include-example.ini'), ('flowers', 'foo.ini')]
+
+The manifests should be there too::
+
+    >>> len(parser.manifests())
+    3
+
+We're already in the root directory::
+
+    >>> os.getcwd() == parser.rootdir
+    True
+
+DEFAULT values should persist across includes, unless they're
+overwritten.  In this example, include-example.ini sets foo=bar, but
+its overridden to fleem in bar.ini::
+
+    >>> parser.get('name', foo='bar')
+    ['fleem', 'flowers']
+    >>> parser.get('name', foo='fleem')
+    ['crash-handling']
+
+Passing parameters in the include section allows defining variables in
+the submodule scope:
+
+    >>> parser.get('name', tags=['red'])
+    ['flowers']
+
+However, this should be overridable from the DEFAULT section in the
+included file and that overridable via the key directly connected to
+the test::
+
+    >>> parser.get(name='flowers')[0]['blue']
+    'ocean'
+    >>> parser.get(name='flowers')[0]['yellow']
+    'submarine'
+
+You can query multiple times if you need to::
+
+    >>> flowers = parser.get(foo='bar')
+    >>> len(flowers)
+    2
+    >>> roses = parser.get(tests=flowers, red='roses')
+
+Using the inverse flag should invert the set of tests returned::
+
+    >>> parser.get('name', inverse=True, tags=['red'])
+    ['crash-handling', 'fleem']
+
+All of the included tests actually exist::
+
+    >>> [i['name'] for i in parser.missing()]
+    []
+
+Write the output to a manifest:
+
+    >>> from StringIO import StringIO
+    >>> buffer = StringIO()
+    >>> parser.write(fp=buffer, global_kwargs={'foo': 'bar'})
+    >>> buffer.getvalue().strip()
+    '[DEFAULT]\nfoo = bar\n\n[fleem]\n\n[include/flowers]\nblue = ocean\nred = roses\nyellow = submarine'
+
+Test our ability to convert a static directory structure to a
+manifest. First, stub out a directory with files in it::
+
+    >>> import shutil, tempfile
+    >>> def create_stub():
+    ...     directory = tempfile.mkdtemp()
+    ...     for i in 'foo', 'bar', 'fleem':
+    ...         file(os.path.join(directory, i), 'w').write(i)
+    ...     subdir = os.path.join(directory, 'subdir')
+    ...     os.mkdir(subdir)
+    ...     file(os.path.join(subdir, 'subfile'), 'w').write('baz')
+    ...     return directory
+    >>> stub = create_stub()
+    >>> os.path.exists(stub) and os.path.isdir(stub)
+    True
+
+Make a manifest for it::
+
+    >>> from manifestparser import convert
+    >>> print convert([stub])
+    [bar]
+    [fleem]
+    [foo]
+    [subdir/subfile]
+    >>> shutil.rmtree(stub)
+
+Now do the same thing but keep the manifests in place::
+
+    >>> stub = create_stub()
+    >>> convert([stub], write='manifest.ini')
+    >>> sorted(os.listdir(stub))
+    ['bar', 'fleem', 'foo', 'manifest.ini', 'subdir']
+    >>> parser = ManifestParser()
+    >>> parser.read(os.path.join(stub, 'manifest.ini'))
+    >>> [i['name'] for i in parser.tests]
+    ['subfile', 'bar', 'fleem', 'foo']
+    >>> parser = ManifestParser()
+    >>> parser.read(os.path.join(stub, 'subdir', 'manifest.ini'))
+    >>> len(parser.tests)
+    1
+    >>> parser.tests[0]['name']
+    'subfile'
+    >>> shutil.rmtree(stub)
+
+Test our ability to copy a set of manifests::
+
+    >>> tempdir = tempfile.mkdtemp()
+    >>> manifest = ManifestParser(manifests=('include-example.ini',))
+    >>> manifest.copy(tempdir)
+    >>> sorted(os.listdir(tempdir))
+    ['fleem', 'include', 'include-example.ini']
+    >>> sorted(os.listdir(os.path.join(tempdir, 'include')))
+    ['bar.ini', 'crash-handling', 'flowers', 'foo.ini']
+    >>> from_manifest = ManifestParser(manifests=('include-example.ini',))
+    >>> to_manifest = os.path.join(tempdir, 'include-example.ini')
+    >>> to_manifest = ManifestParser(manifests=(to_manifest,))
+    >>> to_manifest.get('name') == from_manifest.get('name')
+    True
+    >>> shutil.rmtree(tempdir)
+
+Test our ability to update tests from a manifest and a directory of
+files::
+
+    >>> tempdir = tempfile.mkdtemp()
+    >>> for i in range(10):
+    ...     file(os.path.join(tempdir, str(i)), 'w').write(str(i))
+
+First, make a manifest::
+
+    >>> manifest = convert([tempdir])
+    >>> newtempdir = tempfile.mkdtemp()
+    >>> manifest_file = os.path.join(newtempdir, 'manifest.ini')
+    >>> file(manifest_file,'w').write(manifest)
+    >>> manifest = ManifestParser(manifests=(manifest_file,))
+    >>> manifest.get('name') == [str(i) for i in range(10)]
+    True
+
+All of the tests are initially missing::
+
+    >>> [i['name'] for i in manifest.missing()] == [str(i) for i in range(10)]
+    True
+
+But then we copy one over::
+
+    >>> manifest.get('name', name='1')
+    ['1']
+    >>> manifest.update(tempdir, name='1')
+    >>> sorted(os.listdir(newtempdir))
+    ['1', 'manifest.ini']
+
+Update that one file and copy all the "tests"::
+   
+    >>> file(os.path.join(tempdir, '1'), 'w').write('secret door')
+    >>> manifest.update(tempdir)
+    >>> sorted(os.listdir(newtempdir))
+    ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'manifest.ini']
+    >>> file(os.path.join(newtempdir, '1')).read().strip()
+    'secret door'
+
+Clean up::
+
+    >>> shutil.rmtree(tempdir)
+    >>> shutil.rmtree(newtempdir)
+
+You can override the path in the section too.  This shows that you can
+use a relative path::
+
+    >>> manifest = ManifestParser(manifests=('path-example.ini',))
+    >>> manifest.tests[0]['path'] == os.path.abspath('fleem')
+    True
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/manifestdestiny/tests/test_testmanifest.txt
@@ -0,0 +1,32 @@
+Test the Test Manifest
+======================
+
+Boilerplate::
+
+    >>> import os
+
+Test filtering based on platform::
+
+    >>> from manifestparser import TestManifest
+    >>> manifest = TestManifest(manifests=('filter-example.ini',))
+    >>> [i['name'] for i in manifest.active_tests(os='win', disabled=False, exists=False)]
+    ['windowstest', 'fleem']
+    >>> [i['name'] for i in manifest.active_tests(os='linux', disabled=False, exists=False)]
+    ['fleem', 'linuxtest']
+
+Look for existing tests.  There is only one::
+
+    >>> [i['name'] for i in manifest.active_tests()]
+    ['fleem']
+
+You should be able to expect failures::
+
+    >>> last_test = manifest.active_tests(exists=False, toolkit='gtk2')[-1]
+    >>> last_test['name']
+    'linuxtest'
+    >>> last_test['expected']
+    'pass'
+    >>> last_test = manifest.active_tests(exists=False, toolkit='cocoa')[-1]
+    >>> last_test['expected']
+    'fail'
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozhttpd/README.md
@@ -0,0 +1,1 @@
+basic python webserver, tested with talos
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozhttpd/mozhttpd.py
@@ -0,0 +1,172 @@
+#!/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/setup.py
@@ -0,0 +1,72 @@
+# ***** 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 mozhttpd.
+#
+# 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 <jmaher@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
+
+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'
+
+deps = []
+
+setup(name='mozhttpd',
+      version=version,
+      description="basic python webserver, tested with talos",
+      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=[],
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=deps,
+      entry_points="""
+      # -*- Entry points: -*-
+      [console_scripts]
+      mozhttpd = mozhttpd:main
+      """,
+      )
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinfo/README.md
@@ -0,0 +1,62 @@
+Throughout [mozmill](https://developer.mozilla.org/en/Mozmill)
+and other Mozilla python code, checking the underlying
+platform is done in many different ways.  The various checks needed
+lead to a lot of copy+pasting, leaving the reader to wonder....is this
+specific check necessary for (e.g.) an operating system?  Because
+information is not consolidated, checks are not done consistently, nor
+is it defined what we are checking for.
+
+[MozInfo](https://github.com/mozilla/mozbase/tree/master/mozinfo)
+proposes to solve this problem.  MozInfo is a bridge interface,
+making the underlying (complex) plethora of OS and architecture
+combinations conform to a subset of values of relavence to 
+Mozilla software. The current implementation exposes relavent key,
+values: `os`, `version`, `bits`, and `processor`.  Additionally, the
+service pack in use is available on the windows platform.
+
+
+# API Usage
+
+MozInfo is a python package.  Downloading the software and running
+`python setup.py develop` will allow you to do `import mozinfo`
+from python.  
+[mozinfo.py](https://github.com/mozilla/mozbase/blob/master/mozinfo/mozinfo.py)
+is the only file contained is this package,
+so if you need a single-file solution, you can just download or call
+this file through the web.
+
+The top level attributes (`os`, `version`, `bits`, `processor`) are
+available as module globals:
+
+    if mozinfo.os == 'win': ...
+
+In addition, mozinfo exports a dictionary, `mozinfo.info`, that
+contain these values.  mozinfo also exports:
+
+- `choices`: a dictionary of possible values for os, bits, and
+  processor
+- `main`: the console_script entry point for mozinfo
+- `unknown`: a singleton denoting a value that cannot be determined
+
+`unknown` has the string representation `"UNKNOWN"`. unknown will evaluate
+as `False` in python:
+
+    if not mozinfo.os: ... # unknown!
+
+
+# Command Line Usage
+
+MozInfo comes with a command line, `mozinfo` which may be used to
+diagnose one's current system.
+
+Example output:
+
+    os: linux
+    version: Ubuntu 10.10
+    bits: 32
+    processor: x86
+
+Three of these fields, os, bits, and processor, have a finite set of
+choices.  You may display the value of these choices using 
+`mozinfo --os`, `mozinfo --bits`, and `mozinfo --processor`. 
+`mozinfo --help` documents command-line usage.
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/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()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinfo/setup.py
@@ -0,0 +1,78 @@
+# ***** 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) 2011.
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Jeff Hammel <jhammel@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
+
+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):
+    description = ''
+
+# dependencies
+deps = []
+try:
+    import json
+except ImportError:
+    deps = ['simplejson']
+
+setup(name='mozinfo',
+      version=version,
+      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=[],
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=deps,
+      entry_points="""
+      # -*- Entry points: -*-
+      [console_scripts]
+      mozinfo = mozinfo:main
+      """,
+      )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinstall/README.md
@@ -0,0 +1,35 @@
+[Mozinstall](https://github.com/mozilla/mozbase/tree/master/mozinstall)
+is a python package for installing Mozilla applications on various platforms.
+
+For example, depending on the platform, Firefox can be distributed as a 
+zip, tar.bz2, exe or dmg file or cloned from a repository. Mozinstall takes the 
+hassle out of extracting and/or running these files and for convenience returns
+the full path to the application's binary in the install directory. In the case 
+that mozinstall is invoked from the command line, the binary path will be 
+printed to stdout.
+
+# Usage
+
+For command line options run mozinstall --help
+
+Mozinstall's main function is the install method
+
+    import mozinstall
+    mozinstall.install('path_to_install_file', dest='path_to_install_folder')
+
+The dest parameter defaults to the directory in which the install file is located.
+The install method accepts a third parameter called apps which tells mozinstall which 
+binary to search for. By default it will search for 'firefox', 'thunderbird' and 'fennec'
+so unless you are installing a different application, this parameter is unnecessary.
+
+# Error Handling
+
+Mozinstall throws two different types of exceptions:
+
+- mozinstall.InvalidSource is thrown when the source is not a recognized file type (zip, exe, tar.bz2, tar.gz, dmg)
+- mozinstall.InstallError is thrown when the installation fails for any reason. A traceback is provided.
+
+# Dependencies
+
+Mozinstall depends on the [mozinfo](https://github.com/mozilla/mozbase/tree/master/mozinfo) 
+package which is also found in the mozbase repository.
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/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())
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozinstall/setup.py
@@ -0,0 +1,79 @@
+# ***** 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 *****
+
+import os
+from setuptools import setup
+
+try:
+    here = os.path.dirname(os.path.abspath(__file__))
+    description = file(os.path.join(here, 'README.md')).read()
+except IOError:
+    description = None
+
+version = '0.3'
+
+deps = ['mozinfo']
+
+setup(name='mozInstall',
+      version=version,
+      description="This is a utility package for installing Mozilla applications on various platforms.",
+      long_description=description,
+      classifiers=['Environment :: Console',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
+                   'Natural Language :: English',
+                   'Operating System :: OS Independent',
+                   '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=[],
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=deps,
+      entry_points="""
+      # -*- Entry points: -*-
+      [console_scripts]
+      mozinstall = mozinstall:cli
+      """,
+      )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozlog/README.md
@@ -0,0 +1,18 @@
+[Mozlog](https://github.com/mozilla/mozbase/tree/master/mozlog)
+is a python package intended to simplify and standardize logs in the Mozilla universe. 
+It wraps around python's [logging](http://docs.python.org/library/logging.html) 
+module and adds some additional functionality.
+
+# Usage
+
+Import mozlog instead of [logging](http://docs.python.org/library/logging.html) 
+(all functionality in the logging module is also available from the mozlog module). 
+To get a logger, call mozlog.getLogger passing in a name and the path to a log file.
+If no log file is specified, the logger will log to stdout.
+
+    import mozlog
+    logger = mozlog.getLogger('LOG_NAME', 'log_file_path')
+    logger.setLevel(mozlog.DEBUG)
+    logger.info('foo')
+    logger.testPass('bar')
+    mozlog.shutdown()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozlog/mozlog/__init__.py
@@ -0,0 +1,36 @@
+# ***** 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 mozlog.
+#
+# 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 *****
+from logger import *
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozlog/mozlog/logger.py
@@ -0,0 +1,125 @@
+# ***** 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 mozlog.
+#
+# 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 *****
+from logging import getLogger as getSysLogger
+from logging import *
+
+_default_level = INFO
+_LoggerClass = getLoggerClass()
+
+# Define mozlog specific log levels
+START      = _default_level + 1
+END        = _default_level + 2
+PASS       = _default_level + 3
+KNOWN_FAIL = _default_level + 4
+FAIL       = _default_level + 5
+# Define associated text of log levels
+addLevelName(START, 'TEST-START')
+addLevelName(END, 'TEST-END')
+addLevelName(PASS, 'TEST-PASS')
+addLevelName(KNOWN_FAIL, 'TEST-KNOWN-FAIL')
+addLevelName(FAIL, 'TEST-UNEXPECTED-FAIL')
+
+class _MozLogger(_LoggerClass):
+    """
+    MozLogger class which adds three convenience log levels
+    related to automated testing in Mozilla
+    """
+    def testStart(self, message, *args, **kwargs):
+        self.log(START, message, *args, **kwargs)
+
+    def testEnd(self, message, *args, **kwargs):
+        self.log(END, message, *args, **kwargs)
+
+    def testPass(self, message, *args, **kwargs):
+        self.log(PASS, message, *args, **kwargs)
+
+    def testFail(self, message, *args, **kwargs):
+        self.log(FAIL, message, *args, **kwargs)
+
+    def testKnownFail(self, message, *args, **kwargs):
+        self.log(KNOWN_FAIL, message, *args, **kwargs)
+
+class _MozFormatter(Formatter):
+    """
+    MozFormatter class used for default formatting
+    This can easily be overriden with the log handler's setFormatter()
+    """
+    level_length = 0
+    max_level_length = len('TEST-START')
+
+    def __init__(self):
+        pass
+
+    def format(self, record):
+        record.message = record.getMessage()
+
+        # Handles padding so record levels align nicely
+        if len(record.levelname) > self.level_length:
+            pad = 0
+            if len(record.levelname) <= self.max_level_length:
+                self.level_length = len(record.levelname)
+        else:
+            pad = self.level_length - len(record.levelname) + 1
+        sep = '|'.rjust(pad)
+        fmt = '%(name)s %(levelname)s ' + sep + ' %(message)s'
+        return fmt % record.__dict__
+
+def getLogger(name, logfile=None):
+    """
+    Returns the logger with the specified name.
+    If the logger doesn't exist, it is created.
+
+    name       - The name of the logger to retrieve
+    [filePath] - If specified, the logger will log to the specified filePath
+                 Otherwise, the logger logs to stdout
+                 This parameter only has an effect if the logger doesn't exist
+    """
+    setLoggerClass(_MozLogger)
+
+    if name in Logger.manager.loggerDict:
+        return getSysLogger(name)
+
+    logger = getSysLogger(name)
+    logger.setLevel(_default_level)
+
+    if logfile:
+        handler = FileHandler(logfile)
+    else:
+        handler = StreamHandler()
+    handler.setFormatter(_MozFormatter())
+    logger.addHandler(handler)
+    return logger
+
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozlog/setup.py
@@ -0,0 +1,70 @@
+# ***** 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 mozlog.
+#
+# 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 *****
+
+import os
+import sys
+from setuptools import setup, find_packages
+
+PACKAGE_NAME = "mozlog"
+PACKAGE_VERSION = "1.0"
+
+desc = """Robust log handling specialized for logging in the Mozilla universe"""
+# take description from README
+here = os.path.dirname(os.path.abspath(__file__))
+try:
+    description = file(os.path.join(here, 'README.md')).read()
+except IOError, OSError:
+    description = ''
+
+setup(name=PACKAGE_NAME,
+      version=PACKAGE_VERSION,
+      description=desc,
+      long_description=description,
+      author='Andrew Halberstadt, Mozilla',
+      author_email='halbersa@gmail.com',
+      url='http://github.com/ahal/mozbase',
+      license='MPL 1.1/GPL 2.0/LGPL 2.1',
+      packages=find_packages(exclude=['legacy']),
+      zip_safe=False,
+      platforms =['Any'],
+      classifiers=['Development Status :: 4 - Beta',
+                   'Environment :: Console',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
+                   'Operating System :: OS Independent',
+                   'Topic :: Software Development :: Libraries :: Python Modules',
+                  ]
+     )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/README.md
@@ -0,0 +1,34 @@
+[mozprocess](https://github.com/mozilla/mozbase/tree/master/mozprocess)
+provides python process management via an operating system 
+and platform transparent interface to Mozilla platforms of interest.
+Mozprocess aims to provide the ability
+to robustly terminate a process (by timeout or otherwise), along with
+any child processes, on Windows, OS X, and Linux. Mozprocess utilizes
+and extends `subprocess.Popen` to these ends.
+
+
+# API
+
+[mozprocess.processhandler:ProcessHandler](https://github.com/mozilla/mozbase/blob/master/mozprocess/mozprocess/processhandler.py)
+is the central exposed API for mozprocess.  `ProcessHandler` utilizes
+a contained subclass of [subprocess.Popen](http://docs.python.org/library/subprocess.html),
+`Process`, which does the brunt of the process management.
+
+Basic usage:
+
+    process = ProcessHandler(['command', '-line', 'arguments'],
+                             cwd=None, # working directory for cmd; defaults to None
+                             env={},   # environment to use for the process; defaults to os.environ
+                             )         
+    exit_code = process.waitForFinish(timeout=60) # seconds
+
+See an example in https://github.com/mozilla/mozbase/blob/master/mutt/mutt/tests/python/testprofilepath.py
+
+`ProcessHandler` may be subclassed to handle process timeouts (by overriding
+the `onTimeout()` method), process completion (by overriding 
+`onFinish()`), and to process the command output (by overriding 
+`processOutputLine()`).
+
+# TODO
+
+- Document improvements over `subprocess.Popen.kill`
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/mozprocess/__init__.py
@@ -0,0 +1,40 @@
+# ***** 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 mozprocess.
+#
+# 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>
+#  Jonathan Griffin <jgriffin@mozilla.com>
+#  Jeff Hammel <jhammel@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 processhandler import *
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/mozprocess/pid.py
@@ -0,0 +1,117 @@
+#!/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 mozprocess.
+#
+# 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>
+#  Jonathan Griffin <jgriffin@mozilla.com>
+#  Jeff Hammel <jhammel@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
+import mozinfo
+import shlex
+import subprocess
+import sys
+
+# determine the platform-specific invocation of `ps`
+if mozinfo.isMac:
+    psarg = '-Acj'
+elif mozinfo.isLinux:
+    psarg = 'axwww'
+else:
+    psarg = 'ax'
+
+def ps(arg=psarg):
+    """
+    python front-end to `ps`
+    http://en.wikipedia.org/wiki/Ps_%28Unix%29
+    returns a list of process dicts based on the `ps` header
+    """
+    retval = []
+    process = subprocess.Popen(['ps', arg], stdout=subprocess.PIPE)
+    stdout, _ = process.communicate()
+    header = None
+    for line in stdout.splitlines():
+        line = line.strip()
+        if header is None:
+            # first line is the header
+            header = line.split()
+            continue
+        split = line.split(None, len(header)-1)
+        process_dict = dict(zip(header, split))
+        retval.append(process_dict)
+    return retval
+
+def running_processes(name, psarg=psarg, defunct=True):
+    """
+    returns a list of
+    {'PID': PID of process (int)
+     'command': command line of process (list)}
+     with the executable named `name`.
+     - defunct: whether to return defunct processes
+    """
+    retval = []
+    for process in ps(psarg):
+        command = process['COMMAND']
+        command = shlex.split(command)
+        if command[-1] == '<defunct>':
+            command = command[:-1]
+            if not command or not defunct:
+                continue
+        if 'STAT' in process and not defunct:
+            if process['STAT'] == 'Z+':
+                continue
+        prog = command[0]
+        basename = os.path.basename(prog)
+        if basename == name:
+            retval.append((int(process['PID']), command))
+    return retval
+
+def get_pids(name):
+    """Get all the pids matching name"""
+
+    if mozinfo.isWin:
+        # use the windows-specific implementation
+        import wpk
+        return wpk.get_pids(name)
+    else:
+        return [pid for pid,_ in running_processes(name)]
+
+if __name__ == '__main__':
+    pids = set()
+    for i in sys.argv[1:]:
+        pids.update(get_pids(i))
+    for i in sorted(pids):
+        print i
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/mozprocess/processhandler.py
@@ -0,0 +1,758 @@
+# ***** 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 mozprocess.
+#
+# 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>
+#  Jonathan Griffin <jgriffin@mozilla.com>
+#  Jeff Hammel <jhammel@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 logging
+import mozinfo
+import os
+import select
+import signal
+import subprocess
+import sys
+import threading
+import time
+from Queue import Queue
+from datetime import datetime, timedelta
+
+__all__ = ['ProcessHandlerMixin', 'ProcessHandler']
+
+if mozinfo.isWin:
+    import ctypes, ctypes.wintypes, msvcrt
+    from ctypes import sizeof, addressof, c_ulong, byref, POINTER, WinError
+    import winprocess
+    from qijo import JobObjectAssociateCompletionPortInformation, JOBOBJECT_ASSOCIATE_COMPLETION_PORT
+
+class ProcessHandlerMixin(object):
+    """Class which represents a process to be executed."""
+
+    class Process(subprocess.Popen):
+        """
+        Represents our view of a subprocess.
+        It adds a kill() method which allows it to be stopped explicitly.
+        """
+
+        MAX_IOCOMPLETION_PORT_NOTIFICATION_DELAY = 180
+        MAX_PROCESS_KILL_DELAY = 30
+
+        def __init__(self,
+                     args,
+                     bufsize=0,
+                     executable=None,
+                     stdin=None,
+                     stdout=None,
+                     stderr=None,
+                     preexec_fn=None,
+                     close_fds=False,
+                     shell=False,
+                     cwd=None,
+                     env=None,
+                     universal_newlines=False,
+                     startupinfo=None,
+                     creationflags=0,
+                     ignore_children=False):
+
+            # Parameter for whether or not we should attempt to track child processes
+            self._ignore_children = ignore_children
+
+            if not self._ignore_children and not mozinfo.isWin:
+                # Set the process group id for linux systems
+                # Sets process group id to the pid of the parent process
+                # NOTE: This prevents you from using preexec_fn and managing
+                #       child processes, TODO: Ideally, find a way around this
+                def setpgidfn():
+                    os.setpgid(0, 0)
+                preexec_fn = setpgidfn
+
+            try:
+                subprocess.Popen.__init__(self, args, bufsize, executable,
+                                          stdin, stdout, stderr,
+                                          preexec_fn, close_fds,
+                                          shell, cwd, env,
+                                          universal_newlines, startupinfo, creationflags)
+            except OSError, e:
+                print >> sys.stderr, args
+                raise
+
+        def __del__(self, _maxint=sys.maxint):
+            if mozinfo.isWin:
+                if self._handle:
+                    self._internal_poll(_deadstate=_maxint)
+                if self._handle or self._job or self._io_port:
+                    self._cleanup()
+            else:
+                subprocess.Popen.__del__(self)
+
+        def kill(self):
+            self.returncode = 0
+            if mozinfo.isWin:
+                if not self._ignore_children and self._handle and self._job:
+                    winprocess.TerminateJobObject(self._job, winprocess.ERROR_CONTROL_C_EXIT)
+                    self.returncode = winprocess.GetExitCodeProcess(self._handle)
+                elif self._handle:
+                    try:
+                        winprocess.TerminateProcess(self._handle, winprocess.ERROR_CONTROL_C_EXIT)
+                    except:
+                        raise OSError("Could not terminate process")
+                    finally:
+                        self.returncode = winprocess.GetExitCodeProcess(self._handle)
+                        self._cleanup()
+                else:
+                    pass
+            else:
+                if not self._ignore_children:
+                    try:
+                        os.killpg(self.pid, signal.SIGKILL)
+                    except BaseException, e:
+                        if getattr(e, "errno", None) != 3:
+                            # Error 3 is "no such process", which is ok
+                            print >> sys.stderr, "Could not kill process, could not find pid: %s" % self.pid
+                    finally:
+                        # Try to get the exit status
+                        if self.returncode is None:
+                            self.returncode = subprocess.Popen._internal_poll(self)
+
+                else:
+                    os.kill(self.pid, signal.SIGKILL)
+                    if self.returncode is None:
+                        self.returncode = subprocess.Popen._internal_poll(self)
+
+            self._cleanup()
+            return self.returncode
+
+        def wait(self):
+            """ Popen.wait
+                Called to wait for a running process to shut down and return
+                its exit code
+                Returns the main process's exit code
+            """
+            # This call will be different for each OS
+            self.returncode = self._wait()
+            self._cleanup()
+            return self.returncode
+
+        """ Private Members of Process class """
+
+        if mozinfo.isWin:
+            # Redefine the execute child so that we can track process groups
+            def _execute_child(self, args, executable, preexec_fn, close_fds,
+                               cwd, env, universal_newlines, startupinfo,
+                               creationflags, shell,
+                               p2cread, p2cwrite,
+                               c2pread, c2pwrite,
+                               errread, errwrite):
+                if not isinstance(args, basestring):
+                    args = subprocess.list2cmdline(args)
+
+                # Always or in the create new process group
+                creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP
+
+                if startupinfo is None:
+                    startupinfo = winprocess.STARTUPINFO()
+
+                if None not in (p2cread, c2pwrite, errwrite):
+                    startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES
+                    startupinfo.hStdInput = int(p2cread)
+                    startupinfo.hStdOutput = int(c2pwrite)
+                    startupinfo.hStdError = int(errwrite)
+                if shell:
+                    startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW
+                    startupinfo.wShowWindow = winprocess.SW_HIDE
+                    comspec = os.environ.get("COMSPEC", "cmd.exe")
+                    args = comspec + " /c " + args
+
+                # determine if we can create create a job
+                canCreateJob = winprocess.CanCreateJobObject()
+
+                # Ensure we write a warning message if we are falling back
+                if not canCreateJob and not self._ignore_children:
+                    # We can't create job objects AND the user wanted us to
+                    # Warn the user about this.
+                    print >> sys.stderr, "ProcessManager UNABLE to use job objects to manage child processes"
+
+                # set process creation flags
+                creationflags |= winprocess.CREATE_SUSPENDED
+                creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT
+                if canCreateJob:
+                    creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB
+                else:
+                    # Since we've warned, we just log info here to inform you
+                    # of the consequence of setting ignore_children = True
+                    print "ProcessManager NOT managing child processes"
+
+                # create the process
+                hp, ht, pid, tid = winprocess.CreateProcess(
+                    executable, args,
+                    None, None, # No special security
+                    1, # Must inherit handles!
+                    creationflags,
+                    winprocess.EnvironmentBlock(env),
+                    cwd, startupinfo)
+                self._child_created = True
+                self._handle = hp
+                self._thread = ht
+                self.pid = pid
+                self.tid = tid
+
+                if canCreateJob:
+                    try:
+                        # We create a new job for this process, so that we can kill
+                        # the process and any sub-processes
+                        # Create the IO Completion Port
+                        self._io_port = winprocess.CreateIoCompletionPort()
+                        self._job = winprocess.CreateJobObject()
+
+                        # Now associate the io comp port and the job object
+                        joacp = JOBOBJECT_ASSOCIATE_COMPLETION_PORT(winprocess.COMPKEY_JOBOBJECT,
+                                                                    self._io_port)
+                        winprocess.SetInformationJobObject(self._job,
+                                                          JobObjectAssociateCompletionPortInformation,
+                                                          addressof(joacp),
+                                                          sizeof(joacp)
+                                                          )
+
+                        # Assign the job object to the process
+                        winprocess.AssignProcessToJobObject(self._job, int(hp))
+
+                        # It's overkill, but we use Queue to signal between threads
+                        # because it handles errors more gracefully than event or condition.
+                        self._process_events = Queue()
+
+                        # Spin up our thread for managing the IO Completion Port
+                        self._procmgrthread = threading.Thread(target = self._procmgr)
+                    except:
+                        print >> sys.stderr, """Exception trying to use job objects;
+falling back to not using job objects for managing child processes"""
+                        # Ensure no dangling handles left behind
+                        self._cleanup_job_io_port()
+                else:
+                    self._job = None
+
+                winprocess.ResumeThread(int(ht))
+                if self._procmgrthread:
+                    self._procmgrthread.start()
+                ht.Close()
+
+                for i in (p2cread, c2pwrite, errwrite):
+                    if i is not None:
+                        i.Close()
+
+            # Windows Process Manager - watches the IO Completion Port and
+            # keeps track of child processes
+            def _procmgr(self):
+                if not (self._io_port) or not (self._job):
+                    return
+
+                try:
+                    self._poll_iocompletion_port()
+                except KeyboardInterrupt:
+                    raise KeyboardInterrupt
+
+            def _poll_iocompletion_port(self):
+                # Watch the IO Completion port for status
+                self._spawned_procs = {}
+                countdowntokill = 0
+
+                while True:
+                    msgid = c_ulong(0)
+                    compkey = c_ulong(0)
+                    pid = c_ulong(0)
+                    portstatus = winprocess.GetQueuedCompletionStatus(self._io_port,
+                                                                      byref(msgid),
+                                                                      byref(compkey),
+                                                                      byref(pid),
+                                                                      5000)
+
+                    # If the countdowntokill has been activated, we need to check
+                    # if we should start killing the children or not.
+                    if countdowntokill != 0:
+                        diff = datetime.now() - countdowntokill
+                        # Arbitrarily wait 3 minutes for windows to get its act together
+                        # Windows sometimes takes a small nap between notifying the
+                        # IO Completion port and actually killing the children, and we
+                        # don't want to mistake that situation for the situation of an unexpected
+                        # parent abort (which is what we're looking for here).
+                        if diff.seconds > self.MAX_IOCOMPLETION_PORT_NOTIFICATION_DELAY:
+                            print >> sys.stderr, "Parent process exited without \
+                                                  killing children, attempting to kill children"
+                            self.kill()
+                            self._process_events.put({self.pid: 'FINISHED'})
+
+                    if not portstatus:
+                        # Check to see what happened
+                        errcode = winprocess.GetLastError()
+                        if errcode == winprocess.ERROR_ABANDONED_WAIT_0:
+                            # Then something has killed the port, break the loop
+                            print >> sys.stderr, "IO Completion Port unexpectedly closed"
+                            break
+                        elif errcode == winprocess.WAIT_TIMEOUT:
+                            # Timeouts are expected, just keep on polling
+                            continue
+                        else:
+                            print >> sys.stderr, "Error Code %s trying to query IO Completion Port, exiting" % errcode
+                            raise WinError(errcode)
+                            break
+
+                    if compkey.value == winprocess.COMPKEY_TERMINATE.value:
+                        # Then we're done
+                        break
+
+                    # Check the status of the IO Port and do things based on it
+                    if compkey.value == winprocess.COMPKEY_JOBOBJECT.value:
+                        if msgid.value == winprocess.JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO:
+                            # No processes left, time to shut down
+                            # Signal anyone waiting on us that it is safe to shut down
+                            self._process_events.put({self.pid: 'FINISHED'})
+                            break
+                        elif msgid.value == winprocess.JOB_OBJECT_MSG_NEW_PROCESS:
+                            # New Process started
+                            # Add the child proc to our list in case our parent flakes out on us
+                            # without killing everything.
+                            if pid.value != self.pid:
+                                self._spawned_procs[pid.value] = 1
+                        elif msgid.value == winprocess.JOB_OBJECT_MSG_EXIT_PROCESS:
+                            # One process exited normally
+                            if pid.value == self.pid and len(self._spawned_procs) > 0:
+                                # Parent process dying, start countdown timer
+                                countdowntokill = datetime.now()
+                            elif pid.value in self._spawned_procs:
+                                # Child Process died remove from list
+                                del(self._spawned_procs[pid.value])
+                        elif msgid.value == winprocess.JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:
+                            # One process existed abnormally
+                            if pid.value == self.pid and len(self._spawned_procs) > 0:
+                                # Parent process dying, start countdown timer
+                                countdowntokill = datetime.now()
+                            elif pid.value in self._spawned_procs:
+                                # Child Process died remove from list
+                                del self._spawned_procs[pid.value]
+                        else:
+                            # We don't care about anything else
+                            pass
+
+            def _wait(self):
+
+                # First, check to see if the process is still running
+                if self._handle:
+                    self.returncode = winprocess.GetExitCodeProcess(self._handle)
+                else:
+                    # Dude, the process is like totally dead!
+                    return self.returncode
+
+                if self._job and self._procmgrthread.is_alive():
+                    # Then we are managing with IO Completion Ports
+                    # wait on a signal so we know when we have seen the last
+                    # process come through.
+                    # We use queues to synchronize between the thread and this
+                    # function because events just didn't have robust enough error
+                    # handling on pre-2.7 versions
+                    try:
+                        # timeout is the max amount of time the procmgr thread will wait for
+                        # child processes to shutdown before killing them with extreme prejudice.
+                        item = self._process_events.get(timeout=self.MAX_IOCOMPLETION_PORT_NOTIFICATION_DELAY +
+                                                                self.MAX_PROCESS_KILL_DELAY)
+                        if item[self.pid] == 'FINISHED':
+                            self._process_events.task_done()
+                    except:
+                        raise OSError("IO Completion Port failed to signal process shutdown")
+                    finally:
+                        # Either way, let's try to get this code
+                        self.returncode = winprocess.GetExitCodeProcess(self._handle)
+                        self._cleanup()
+
+                else:
+                    # Not managing with job objects, so all we can reasonably do
+                    # is call waitforsingleobject and hope for the best
+
+                    # First, make sure we have not already ended
+                    if self.returncode != winprocess.STILL_ACTIVE:
+                        self._cleanup()
+                        return self.returncode
+
+                    rc = None
+                    if self._handle:
+                        rc = winprocess.WaitForSingleObject(self._handle, -1)
+
+                    if rc == winprocess.WAIT_TIMEOUT:
+                        # The process isn't dead, so kill it
+                        print "Timed out waiting for process to close, attempting TerminateProcess"
+                        self.kill()
+                    elif rc == winprocess.WAIT_OBJECT_0:
+                        # We caught WAIT_OBJECT_0, which indicates all is well
+                        print "Single process terminated successfully"
+                        self.returncode = winprocess.GetExitCodeProcess(self._handle)
+                    else:
+                        # An error occured we should probably throw
+                        rc = winprocess.GetLastError()
+                        if rc:
+                            raise WinError(rc)
+
+                    self._cleanup()
+                    return self.returncode
+
+            def _cleanup_job_io_port(self):
+                """ Do the job and IO port cleanup separately because there are
+                    cases where we want to clean these without killing _handle
+                    (i.e. if we fail to create the job object in the first place)
+                """
+                if self._job and self._job != winprocess.INVALID_HANDLE_VALUE:
+                    self._job.Close()
+                    self._job = None
+                else:
+                    # If windows already freed our handle just set it to none
+                    # (saw this intermittently while testing)
+                    self._job = None
+
+                if self._io_port and self._io_port != winprocess.INVALID_HANDLE_VALUE:
+                    self._io_port.Close()
+                    self._io_port = None
+                else:
+                    self._io_port = None
+
+                if self._procmgrthread:
+                    self._procmgrthread = None
+
+            def _cleanup(self):
+                self._cleanup_job_io_port()
+                if self._thread and self._thread != winprocess.INVALID_HANDLE_VALUE:
+                    self._thread.Close()
+                    self._thread = None
+                else:
+                    self._thread = None
+
+                if self._handle and self._handle != winprocess.INVALID_HANDLE_VALUE:
+                    self._handle.Close()
+                    self._handle = None
+                else:
+                    self._handle = None
+
+        elif mozinfo.isMac or mozinfo.isUnix:
+
+            def _wait(self):
+                """ Haven't found any reason to differentiate between these platforms
+                    so they all use the same wait callback.  If it is necessary to
+                    craft different styles of wait, then a new _wait method
+                    could be easily implemented.
+                """
+
+                if not self._ignore_children:
+                    try:
+                        # os.waitpid returns a (pid, status) tuple
+                        return os.waitpid(self.pid, 0)[1]
+                    except OSError, e:
+                        if getattr(e, "errno", None) != 10:
+                            # Error 10 is "no child process", which could indicate normal
+                            # close
+                            print >> sys.stderr, "Encountered error waiting for pid to close: %s" % e
+                            raise
+                        return 0
+
+                else:
+                    # For non-group wait, call base class
+                    subprocess.Popen.wait(self)
+                    return self.returncode
+
+            def _cleanup(self):
+                pass
+
+        else:
+            # An unrecognized platform, we will call the base class for everything
+            print >> sys.stderr, "Unrecognized platform, process groups may not be managed properly"
+
+            def _wait(self):
+                self.returncode = subprocess.Popen.wait(self)
+                return self.returncode
+
+            def _cleanup(self):
+                pass
+
+    def __init__(self,
+                 cmd,
+                 args=None,
+                 cwd=None,
+                 env=os.environ.copy(),
+                 ignore_children = False,
+                 processOutputLine=(),
+                 onTimeout=(),
+                 onFinish=(),
+                 **kwargs):
+        """
+        cmd = Command to run
+        args = array of arguments (defaults to None)
+        cwd = working directory for cmd (defaults to None)
+        env = environment to use for the process (defaults to os.environ)
+        ignore_children = when True, causes system to ignore child processes,
+        defaults to False (which tracks child processes)
+        processOutputLine = handlers to process the output line
+        onTimeout = handlers for timeout event
+        kwargs = keyword args to pass directly into Popen
+
+        NOTE: Child processes will be tracked by default.  If for any reason
+        we are unable to track child processes and ignore_children is set to False,
+        then we will fall back to only tracking the root process.  The fallback
+        will be logged.
+        """
+        self.cmd = cmd
+        self.args = args
+        self.cwd = cwd
+        self.env = env
+        self.didTimeout = False
+        self._ignore_children = ignore_children
+        self.keywordargs = kwargs
+
+        # handlers
+        self.processOutputLineHandlers = list(processOutputLine)
+        self.onTimeoutHandlers = list(onTimeout)
+        self.onFinishHandlers = list(onFinish)
+
+        # It is common for people to pass in the entire array with the cmd and
+        # the args together since this is how Popen uses it.  Allow for that.
+        if not isinstance(self.cmd, list):
+            self.cmd = [self.cmd]
+
+        if self.args:
+            self.cmd = self.cmd + self.args
+
+    @property
+    def timedOut(self):
+        """True if the process has timed out."""
+        return self.didTimeout
+
+    @property
+    def commandline(self):
+        """the string value of the command line"""
+        return subprocess.list2cmdline([self.cmd] + self.args)
+
+    def run(self):
+        """Starts the process.  waitForFinish must be called to allow the
+           process to complete.
+        """
+        self.didTimeout = False
+        self.startTime = datetime.now()
+        self.proc = self.Process(self.cmd,
+                                 stdout=subprocess.PIPE,
+                                 stderr=subprocess.STDOUT,
+                                 cwd=self.cwd,
+                                 env=self.env,
+                                 ignore_children = self._ignore_children,
+                                 **self.keywordargs)
+
+    def kill(self):
+        """
+          Kills the managed process and if you created the process with
+          'ignore_children=False' (the default) then it will also
+          also kill all child processes spawned by it.
+          If you specified 'ignore_children=True' when creating the process,
+          only the root process will be killed.
+
+          Note that this does not manage any state, save any output etc,
+          it immediately kills the process.
+        """
+        return self.proc.kill()
+
+    def readWithTimeout(self, f, timeout):
+        """
+          Try to read a line of output from the file object |f|.
+          |f| must be a  pipe, like the |stdout| member of a subprocess.Popen
+          object created with stdout=PIPE. If no output
+          is received within |timeout| seconds, return a blank line.
+          Returns a tuple (line, did_timeout), where |did_timeout| is True
+          if the read timed out, and False otherwise.
+
+          Calls a private member because this is a different function based on
+          the OS
+        """
+        return self._readWithTimeout(f, timeout)
+
+    def processOutputLine(self, line):
+        """Called for each line of output that a process sends to stdout/stderr.
+        """
+        for handler in self.processOutputLineHandlers:
+            handler(line)
+
+    def onTimeout(self):
+        """Called when a process times out."""
+        for handler in self.onTimeoutHandlers:
+            handler()
+
+    def onFinish(self):
+        """Called when a process finishes without a timeout."""
+        for handler in self.onFinishHandlers:
+            handler()
+
+    def waitForFinish(self, timeout=None, outputTimeout=None):
+        """
+        Handle process output until the process terminates or times out.
+
+        If timeout is not None, the process will be allowed to continue for
+        that number of seconds before being killed.
+
+        If outputTimeout is not None, the process will be allowed to continue
+        for that number of seconds without producing any output before
+        being killed.
+        """
+
+        if not hasattr(self, 'proc'):
+            self.run()
+
+        self.didTimeout = False
+        logsource = self.proc.stdout
+
+        lineReadTimeout = None
+        if timeout:
+            lineReadTimeout = timeout - (datetime.now() - self.startTime).seconds
+        elif outputTimeout:
+            lineReadTimeout = outputTimeout
+
+        (line, self.didTimeout) = self.readWithTimeout(logsource, lineReadTimeout)
+        while line != "" and not self.didTimeout:
+            self.processOutputLine(line.rstrip())
+            if timeout:
+                lineReadTimeout = timeout - (datetime.now() - self.startTime).seconds
+            (line, self.didTimeout) = self.readWithTimeout(logsource, lineReadTimeout)
+
+
+        if self.didTimeout:
+            self.proc.kill()
+            self.onTimeout()
+        else:
+            self.onFinish()
+
+        status = self.proc.wait()
+        return status
+
+
+    ### Private methods from here on down. Thar be dragons.
+
+    if mozinfo.isWin:
+        # Windows Specific private functions are defined in this block
+        PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
+        GetLastError = ctypes.windll.kernel32.GetLastError
+
+        def _readWithTimeout(self, f, timeout):
+            if timeout is None:
+                # shortcut to allow callers to pass in "None" for no timeout.
+                return (f.readline(), False)
+            x = msvcrt.get_osfhandle(f.fileno())
+            l = ctypes.c_long()
+            done = time.time() + timeout
+            while time.time() < done:
+                if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
+                    err = self.GetLastError()
+                    if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
+                        return ('', False)
+                    else:
+                        raise OSError("readWithTimeout got error: %d", err)
+                if l.value > 0:
+                    # we're assuming that the output is line-buffered,
+                    # which is not unreasonable
+                    return (f.readline(), False)
+                time.sleep(0.01)
+            return ('', True)
+
+    else:
+        # Generic
+        def _readWithTimeout(self, f, timeout):
+            try:
+                (r, w, e) = select.select([f], [], [], timeout)
+            except:
+                # TODO: return a blank line?
+                return ('', True)
+
+            if len(r) == 0:
+                return ('', True)
+            return (f.readline(), False)
+
+
+### default output handlers
+### these should be callables that take the output line
+
+def print_output(line):
+    print line
+
+class StoreOutput(object):
+    """accumulate stdout"""
+
+    def __init__(self):
+        self.output = []
+
+    def __call__(self, line):
+        self.output.append(line)
+
+class LogOutput(object):
+    """pass output to a file"""
+
+    def __init__(self, filename):
+        self.filename = filename
+        self.file = None
+
+    def __call__(self, line):
+        if self.file is None:
+            self.file = file(self.filename, 'a')
+        self.file.write(line + '\n')
+        self.file.flush()
+
+    def __del__(self):
+        if self.file is not None:
+            self.file.close()
+
+### front end class with the default handlers
+
+class ProcessHandler(ProcessHandlerMixin):
+
+    def __init__(self, cmd, logfile=None, storeOutput=True, **kwargs):
+        """
+        If storeOutput=True, the output produced by the process will be saved
+        as self.output.
+
+        If logfile is not None, the output produced by the process will be
+        appended to the given file.
+        """
+
+        kwargs.setdefault('processOutputLine', []).append(print_output)
+
+        if logfile:
+            logoutput = LogOutput(logfile)
+            kwargs['processOutputLine'].append(logoutput)
+
+        self.output = None
+        if storeOutput:
+            storeoutput = StoreOutput()
+            self.output = storeoutput.output
+            kwargs['processOutputLine'].append(storeoutput)
+
+        ProcessHandlerMixin.__init__(self, cmd, **kwargs)
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/mozprocess/qijo.py
@@ -0,0 +1,175 @@
+# ***** 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 mozprocess.
+#
+# 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>
+#  Jonathan Griffin <jgriffin@mozilla.com>
+#  Jeff Hammel <jhammel@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 ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE, addressof, c_size_t, c_ulong
+from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LARGE_INTEGER
+
+LPVOID = c_void_p
+LPDWORD = POINTER(DWORD)
+SIZE_T = c_size_t
+ULONG_PTR = POINTER(c_ulong)
+
+# A ULONGLONG is a 64-bit unsigned integer.
+# Thus there are 8 bytes in a ULONGLONG.
+# XXX why not import c_ulonglong ?
+ULONGLONG = BYTE * 8
+
+class IO_COUNTERS(Structure):
+    # The IO_COUNTERS struct is 6 ULONGLONGs.
+    # TODO: Replace with non-dummy fields.
+    _fields_ = [('dummy', ULONGLONG * 6)]
+
+class JOBOBJECT_BASIC_ACCOUNTING_INFORMATION(Structure):
+    _fields_ = [('TotalUserTime', LARGE_INTEGER),
+                ('TotalKernelTime', LARGE_INTEGER),
+                ('ThisPeriodTotalUserTime', LARGE_INTEGER),
+                ('ThisPeriodTotalKernelTime', LARGE_INTEGER),
+                ('TotalPageFaultCount', DWORD),
+                ('TotalProcesses', DWORD),
+                ('ActiveProcesses', DWORD),
+                ('TotalTerminatedProcesses', DWORD)]
+
+class JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION(Structure):
+    _fields_ = [('BasicInfo', JOBOBJECT_BASIC_ACCOUNTING_INFORMATION),
+                ('IoInfo', IO_COUNTERS)]
+
+# see http://msdn.microsoft.com/en-us/library/ms684147%28VS.85%29.aspx
+class JOBOBJECT_BASIC_LIMIT_INFORMATION(Structure):
+    _fields_ = [('PerProcessUserTimeLimit', LARGE_INTEGER),
+                ('PerJobUserTimeLimit', LARGE_INTEGER),
+                ('LimitFlags', DWORD),
+                ('MinimumWorkingSetSize', SIZE_T),
+                ('MaximumWorkingSetSize', SIZE_T),
+                ('ActiveProcessLimit', DWORD),
+                ('Affinity', ULONG_PTR),
+                ('PriorityClass', DWORD),
+                ('SchedulingClass', DWORD)
+                ]
+
+class JOBOBJECT_ASSOCIATE_COMPLETION_PORT(Structure):
+    _fields_ = [('CompletionKey', c_ulong),
+                ('CompletionPort', HANDLE)]
+
+# see http://msdn.microsoft.com/en-us/library/ms684156%28VS.85%29.aspx
+class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(Structure):
+    _fields_ = [('BasicLimitInformation', JOBOBJECT_BASIC_LIMIT_INFORMATION),
+                ('IoInfo', IO_COUNTERS),
+                ('ProcessMemoryLimit', SIZE_T),
+                ('JobMemoryLimit', SIZE_T),
+                ('PeakProcessMemoryUsed', SIZE_T),
+                ('PeakJobMemoryUsed', SIZE_T)]
+
+# These numbers below come from:
+# http://msdn.microsoft.com/en-us/library/ms686216%28v=vs.85%29.aspx
+JobObjectAssociateCompletionPortInformation = 7
+JobObjectBasicAndIoAccountingInformation = 8
+JobObjectExtendedLimitInformation = 9
+
+class JobObjectInfo(object):
+    mapping = { 'JobObjectBasicAndIoAccountingInformation': 8,
+                'JobObjectExtendedLimitInformation': 9,
+                'JobObjectAssociateCompletionPortInformation': 7
+                }
+    structures = {
+                   7: JOBOBJECT_ASSOCIATE_COMPLETION_PORT,
+                   8: JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION,
+                   9: JOBOBJECT_EXTENDED_LIMIT_INFORMATION
+                   }
+    def __init__(self, _class):
+        if isinstance(_class, basestring):
+            assert _class in self.mapping, 'Class should be one of %s; you gave %s' % (self.mapping, _class)
+            _class = self.mapping[_class]
+        assert _class in self.structures, 'Class should be one of %s; you gave %s' % (self.structures, _class)
+        self.code = _class
+        self.info = self.structures[_class]()
+
+
+QueryInformationJobObjectProto = WINFUNCTYPE(
+    BOOL,        # Return type
+    HANDLE,      # hJob
+    DWORD,       # JobObjectInfoClass
+    LPVOID,      # lpJobObjectInfo
+    DWORD,       # cbJobObjectInfoLength
+    LPDWORD      # lpReturnLength
+    )
+
+QueryInformationJobObjectFlags = (
+    (1, 'hJob'),
+    (1, 'JobObjectInfoClass'),
+    (1, 'lpJobObjectInfo'),
+    (1, 'cbJobObjectInfoLength'),
+    (1, 'lpReturnLength', None)
+    )
+
+_QueryInformationJobObject = QueryInformationJobObjectProto(
+    ('QueryInformationJobObject', windll.kernel32),
+    QueryInformationJobObjectFlags
+    )
+
+class SubscriptableReadOnlyStruct(object):
+    def __init__(self, struct):
+        self._struct = struct
+
+    def _delegate(self, name):
+        result = getattr(self._struct, name)
+        if isinstance(result, Structure):
+            return SubscriptableReadOnlyStruct(result)
+        return result
+
+    def __getitem__(self, name):
+        match = [fname for fname, ftype in self._struct._fields_
+                 if fname == name]
+        if match:
+            return self._delegate(name)
+        raise KeyError(name)
+
+    def __getattr__(self, name):
+        return self._delegate(name)
+
+def QueryInformationJobObject(hJob, JobObjectInfoClass):
+    jobinfo = JobObjectInfo(JobObjectInfoClass)
+    result = _QueryInformationJobObject(
+        hJob=hJob,
+        JobObjectInfoClass=jobinfo.code,
+        lpJobObjectInfo=addressof(jobinfo.info),
+        cbJobObjectInfoLength=sizeof(jobinfo.info)
+        )
+    if not result:
+        raise WinError()
+    return SubscriptableReadOnlyStruct(jobinfo.info)
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/mozprocess/winprocess.py
@@ -0,0 +1,457 @@
+# A module to expose various thread/process/job related structures and
+# methods from kernel32
+#
+# The MIT License
+#
+# Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se>
+#
+# Additions and modifications written by Benjamin Smedberg
+# <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation
+# <http://www.mozilla.org/>
+#
+# More Modifications
+# Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com>
+# Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com>
+#
+# By obtaining, using, and/or copying this software and/or its
+# associated documentation, you agree that you have read, understood,
+# and will comply with the following terms and conditions:
+#
+# Permission to use, copy, modify, and distribute this software and
+# its associated documentation for any purpose and without fee is
+# hereby granted, provided that the above copyright notice appears in
+# all copies, and that both that copyright notice and this permission
+# notice appear in supporting documentation, and that the name of the
+# author not be used in advertising or publicity pertaining to
+# distribution of the software without specific, written prior
+# permission.
+#
+# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from ctypes import c_void_p, POINTER, sizeof, Structure, Union, windll, WinError, WINFUNCTYPE, c_ulong
+from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD, ULONG
+from qijo import QueryInformationJobObject
+
+LPVOID = c_void_p
+LPBYTE = POINTER(BYTE)
+LPDWORD = POINTER(DWORD)
+LPBOOL = POINTER(BOOL)
+LPULONG = POINTER(c_ulong)
+
+def ErrCheckBool(result, func, args):
+    """errcheck function for Windows functions that return a BOOL True
+    on success"""
+    if not result:
+        raise WinError()
+    return args
+
+
+# AutoHANDLE
+
+class AutoHANDLE(HANDLE):
+    """Subclass of HANDLE which will call CloseHandle() on deletion."""
+    
+    CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE)
+    CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32))
+    CloseHandle.errcheck = ErrCheckBool
+    
+    def Close(self):
+        if self.value and self.value != HANDLE(-1).value:
+            self.CloseHandle(self)
+            self.value = 0
+    
+    def __del__(self):
+        self.Close()
+
+    def __int__(self):
+        return self.value
+
+def ErrCheckHandle(result, func, args):
+    """errcheck function for Windows functions that return a HANDLE."""
+    if not result:
+        raise WinError()
+    return AutoHANDLE(result)
+
+# PROCESS_INFORMATION structure
+
+class PROCESS_INFORMATION(Structure):
+    _fields_ = [("hProcess", HANDLE),
+                ("hThread", HANDLE),
+                ("dwProcessID", DWORD),
+                ("dwThreadID", DWORD)]
+
+    def __init__(self):
+        Structure.__init__(self)
+        
+        self.cb = sizeof(self)
+
+LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION)
+
+# STARTUPINFO structure
+
+class STARTUPINFO(Structure):
+    _fields_ = [("cb", DWORD),
+                ("lpReserved", LPWSTR),
+                ("lpDesktop", LPWSTR),
+                ("lpTitle", LPWSTR),
+                ("dwX", DWORD),
+                ("dwY", DWORD),
+                ("dwXSize", DWORD),
+                ("dwYSize", DWORD),
+                ("dwXCountChars", DWORD),
+                ("dwYCountChars", DWORD),
+                ("dwFillAttribute", DWORD),
+                ("dwFlags", DWORD),
+                ("wShowWindow", WORD),
+                ("cbReserved2", WORD),
+                ("lpReserved2", LPBYTE),
+                ("hStdInput", HANDLE),
+                ("hStdOutput", HANDLE),
+                ("hStdError", HANDLE)
+                ]
+LPSTARTUPINFO = POINTER(STARTUPINFO)
+
+SW_HIDE                 = 0
+
+STARTF_USESHOWWINDOW    = 0x01
+STARTF_USESIZE          = 0x02
+STARTF_USEPOSITION      = 0x04
+STARTF_USECOUNTCHARS    = 0x08
+STARTF_USEFILLATTRIBUTE = 0x10
+STARTF_RUNFULLSCREEN    = 0x20
+STARTF_FORCEONFEEDBACK  = 0x40
+STARTF_FORCEOFFFEEDBACK = 0x80
+STARTF_USESTDHANDLES    = 0x100
+
+# EnvironmentBlock
+
+class EnvironmentBlock:
+    """An object which can be passed as the lpEnv parameter of CreateProcess.
+    It is initialized with a dictionary."""
+
+    def __init__(self, dict):
+        if not dict:
+            self._as_parameter_ = None
+        else:
+            values = ["%s=%s" % (key, value)
+                      for (key, value) in dict.iteritems()]
+            values.append("")
+            self._as_parameter_ = LPCWSTR("\0".join(values))
+
+# Error Messages we need to watch for go here
+# See: http://msdn.microsoft.com/en-us/library/ms681388%28v=vs.85%29.aspx
+ERROR_ABANDONED_WAIT_0 = 735
+            
+# GetLastError()
+GetLastErrorProto = WINFUNCTYPE(DWORD                   # Return Type
+                               )
+GetLastErrorFlags = ()
+GetLastError = GetLastErrorProto(("GetLastError", windll.kernel32), GetLastErrorFlags)
+
+# CreateProcess()
+
+CreateProcessProto = WINFUNCTYPE(BOOL,                  # Return type
+                                 LPCWSTR,               # lpApplicationName
+                                 LPWSTR,                # lpCommandLine
+                                 LPVOID,                # lpProcessAttributes
+                                 LPVOID,                # lpThreadAttributes
+                                 BOOL,                  # bInheritHandles
+                                 DWORD,                 # dwCreationFlags
+                                 LPVOID,                # lpEnvironment
+                                 LPCWSTR,               # lpCurrentDirectory
+                                 LPSTARTUPINFO,         # lpStartupInfo
+                                 LPPROCESS_INFORMATION  # lpProcessInformation
+                                 )
+
+CreateProcessFlags = ((1, "lpApplicationName", None),
+                      (1, "lpCommandLine"),
+                      (1, "lpProcessAttributes", None),
+                      (1, "lpThreadAttributes", None),
+                      (1, "bInheritHandles", True),
+                      (1, "dwCreationFlags", 0),
+                      (1, "lpEnvironment", None),
+                      (1, "lpCurrentDirectory", None),
+                      (1, "lpStartupInfo"),
+                      (2, "lpProcessInformation"))
+
+def ErrCheckCreateProcess(result, func, args):
+    ErrCheckBool(result, func, args)
+    # return a tuple (hProcess, hThread, dwProcessID, dwThreadID)
+    pi = args[9]
+    return AutoHANDLE(pi.hProcess), AutoHANDLE(pi.hThread), pi.dwProcessID, pi.dwThreadID
+
+CreateProcess = CreateProcessProto(("CreateProcessW", windll.kernel32),
+                                   CreateProcessFlags)
+CreateProcess.errcheck = ErrCheckCreateProcess
+
+# flags for CreateProcess
+CREATE_BREAKAWAY_FROM_JOB = 0x01000000
+CREATE_DEFAULT_ERROR_MODE = 0x04000000
+CREATE_NEW_CONSOLE = 0x00000010
+CREATE_NEW_PROCESS_GROUP = 0x00000200
+CREATE_NO_WINDOW = 0x08000000
+CREATE_SUSPENDED = 0x00000004
+CREATE_UNICODE_ENVIRONMENT = 0x00000400
+
+# Flags for IOCompletion ports (some of these would probably be defined if 
+# we used the win32 extensions for python, but we don't want to do that if we 
+# can help it.
+INVALID_HANDLE_VALUE = HANDLE(-1) # From winbase.h
+
+# Self Defined Constants for IOPort <--> Job Object communication
+COMPKEY_TERMINATE = c_ulong(0)
+COMPKEY_JOBOBJECT = c_ulong(1)
+
+# flags for job limit information
+# see http://msdn.microsoft.com/en-us/library/ms684147%28VS.85%29.aspx
+JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800
+JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000
+
+# Flags for Job Object Completion Port Message IDs from winnt.h
+# See also: http://msdn.microsoft.com/en-us/library/ms684141%28v=vs.85%29.aspx
+JOB_OBJECT_MSG_END_OF_JOB_TIME =          1
+JOB_OBJECT_MSG_END_OF_PROCESS_TIME =      2
+JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT =     3
+JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO =      4
+JOB_OBJECT_MSG_NEW_PROCESS =              6
+JOB_OBJECT_MSG_EXIT_PROCESS =             7
+JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS =    8
+JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT =     9
+JOB_OBJECT_MSG_JOB_MEMORY_LIMIT =         10
+
+# See winbase.h
+DEBUG_ONLY_THIS_PROCESS = 0x00000002
+DEBUG_PROCESS = 0x00000001
+DETACHED_PROCESS = 0x00000008
+    
+# GetQueuedCompletionPortStatus - http://msdn.microsoft.com/en-us/library/aa364986%28v=vs.85%29.aspx
+GetQueuedCompletionStatusProto = WINFUNCTYPE(BOOL,         # Return Type
+                                             HANDLE,       # Completion Port
+                                             LPDWORD,      # Msg ID
+                                             LPULONG,      # Completion Key
+                                             LPULONG,      # PID Returned from the call (may be null)
+                                             DWORD)        # milliseconds to wait
+GetQueuedCompletionStatusFlags = ((1, "CompletionPort", INVALID_HANDLE_VALUE),
+                                  (1, "lpNumberOfBytes", None),
+                                  (1, "lpCompletionKey", None),
+                                  (1, "lpPID", None),
+                                  (1, "dwMilliseconds", 0))
+GetQueuedCompletionStatus = GetQueuedCompletionStatusProto(("GetQueuedCompletionStatus",
+                                                            windll.kernel32),
+                                                           GetQueuedCompletionStatusFlags)
+
+# CreateIOCompletionPort
+# Note that the completion key is just a number, not a pointer.
+CreateIoCompletionPortProto = WINFUNCTYPE(HANDLE,      # Return Type
+                                          HANDLE,      # File Handle
+                                          HANDLE,      # Existing Completion Port
+                                          c_ulong,     # Completion Key
+                                          DWORD        # Number of Threads
+                                         )
+CreateIoCompletionPortFlags = ((1, "FileHandle", INVALID_HANDLE_VALUE),
+                               (1, "ExistingCompletionPort", None),
+                               (1, "CompletionKey", c_ulong(0)),
+                               (1, "NumberOfConcurrentThreads", 0))
+CreateIoCompletionPort = CreateIoCompletionPortProto(("CreateIoCompletionPort",
+                                                      windll.kernel32),
+                                                      CreateIoCompletionPortFlags)
+CreateIoCompletionPort.errcheck = ErrCheckHandle
+
+# SetInformationJobObject
+SetInformationJobObjectProto = WINFUNCTYPE(BOOL,      # Return Type
+                                           HANDLE,    # Job Handle
+                                           DWORD,     # Type of Class next param is
+                                           LPVOID,    # Job Object Class
+                                           DWORD      # Job Object Class Length
+                                          )
+SetInformationJobObjectProtoFlags = ((1, "hJob", None),
+                                     (1, "JobObjectInfoClass", None),
+                                     (1, "lpJobObjectInfo", None),
+                                     (1, "cbJobObjectInfoLength", 0))
+SetInformationJobObject = SetInformationJobObjectProto(("SetInformationJobObject",
+                                                        windll.kernel32),
+                                                        SetInformationJobObjectProtoFlags)
+SetInformationJobObject.errcheck = ErrCheckBool
+
+# CreateJobObject()
+CreateJobObjectProto = WINFUNCTYPE(HANDLE,             # Return type
+                                   LPVOID,             # lpJobAttributes
+                                   LPCWSTR             # lpName
+                                   )
+
+CreateJobObjectFlags = ((1, "lpJobAttributes", None),
+                        (1, "lpName", None))
+
+CreateJobObject = CreateJobObjectProto(("CreateJobObjectW", windll.kernel32),
+                                       CreateJobObjectFlags)
+CreateJobObject.errcheck = ErrCheckHandle
+
+# AssignProcessToJobObject()
+
+AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL,      # Return type
+                                            HANDLE,    # hJob
+                                            HANDLE     # hProcess
+                                            )
+AssignProcessToJobObjectFlags = ((1, "hJob"),
+                                 (1, "hProcess"))
+AssignProcessToJobObject = AssignProcessToJobObjectProto(
+    ("AssignProcessToJobObject", windll.kernel32),
+    AssignProcessToJobObjectFlags)
+AssignProcessToJobObject.errcheck = ErrCheckBool
+
+# GetCurrentProcess()
+# because os.getPid() is way too easy
+GetCurrentProcessProto = WINFUNCTYPE(HANDLE    # Return type
+                                     )
+GetCurrentProcessFlags = ()
+GetCurrentProcess = GetCurrentProcessProto(
+    ("GetCurrentProcess", windll.kernel32),
+    GetCurrentProcessFlags)
+GetCurrentProcess.errcheck = ErrCheckHandle
+
+# IsProcessInJob()
+try:
+    IsProcessInJobProto = WINFUNCTYPE(BOOL,     # Return type
+                                      HANDLE,   # Process Handle
+                                      HANDLE,   # Job Handle
+                                      LPBOOL      # Result
+                                      )
+    IsProcessInJobFlags = ((1, "ProcessHandle"),
+                           (1, "JobHandle", HANDLE(0)),
+                           (2, "Result"))
+    IsProcessInJob = IsProcessInJobProto(
+        ("IsProcessInJob", windll.kernel32),
+        IsProcessInJobFlags)
+    IsProcessInJob.errcheck = ErrCheckBool 
+except AttributeError:
+    # windows 2k doesn't have this API
+    def IsProcessInJob(process):
+        return False
+
+
+# ResumeThread()
+
+def ErrCheckResumeThread(result, func, args):
+    if result == -1:
+        raise WinError()
+
+    return args
+
+ResumeThreadProto = WINFUNCTYPE(DWORD,      # Return type
+                                HANDLE      # hThread
+                                )
+ResumeThreadFlags = ((1, "hThread"),)
+ResumeThread = ResumeThreadProto(("ResumeThread", windll.kernel32),
+                                 ResumeThreadFlags)
+ResumeThread.errcheck = ErrCheckResumeThread
+
+# TerminateProcess()
+
+TerminateProcessProto = WINFUNCTYPE(BOOL,   # Return type
+                                    HANDLE, # hProcess
+                                    UINT    # uExitCode
+                                    )
+TerminateProcessFlags = ((1, "hProcess"),
+                         (1, "uExitCode", 127))
+TerminateProcess = TerminateProcessProto(
+    ("TerminateProcess", windll.kernel32),
+    TerminateProcessFlags)
+TerminateProcess.errcheck = ErrCheckBool
+
+# TerminateJobObject()
+
+TerminateJobObjectProto = WINFUNCTYPE(BOOL,   # Return type
+                                      HANDLE, # hJob
+                                      UINT    # uExitCode
+                                      )
+TerminateJobObjectFlags = ((1, "hJob"),
+                           (1, "uExitCode", 127))
+TerminateJobObject = TerminateJobObjectProto(
+    ("TerminateJobObject", windll.kernel32),
+    TerminateJobObjectFlags)
+TerminateJobObject.errcheck = ErrCheckBool
+
+# WaitForSingleObject()
+
+WaitForSingleObjectProto = WINFUNCTYPE(DWORD,  # Return type
+                                       HANDLE, # hHandle
+                                       DWORD,  # dwMilliseconds
+                                       )
+WaitForSingleObjectFlags = ((1, "hHandle"),
+                            (1, "dwMilliseconds", -1))
+WaitForSingleObject = WaitForSingleObjectProto(
+    ("WaitForSingleObject", windll.kernel32),
+    WaitForSingleObjectFlags)
+
+# http://msdn.microsoft.com/en-us/library/ms681381%28v=vs.85%29.aspx
+INFINITE = -1
+WAIT_TIMEOUT = 0x0102
+WAIT_OBJECT_0 = 0x0
+WAIT_ABANDONED = 0x0080
+
+# http://msdn.microsoft.com/en-us/library/ms683189%28VS.85%29.aspx
+STILL_ACTIVE = 259
+
+# Used when we terminate a process.
+ERROR_CONTROL_C_EXIT = 0x23c
+
+# GetExitCodeProcess()
+
+GetExitCodeProcessProto = WINFUNCTYPE(BOOL,    # Return type
+                                      HANDLE,  # hProcess
+                                      LPDWORD, # lpExitCode
+                                      )
+GetExitCodeProcessFlags = ((1, "hProcess"),
+                           (2, "lpExitCode"))
+GetExitCodeProcess = GetExitCodeProcessProto(
+    ("GetExitCodeProcess", windll.kernel32),
+    GetExitCodeProcessFlags)
+GetExitCodeProcess.errcheck = ErrCheckBool
+
+def CanCreateJobObject():
+    currentProc = GetCurrentProcess()
+    if IsProcessInJob(currentProc):
+        jobinfo = QueryInformationJobObject(HANDLE(0), 'JobObjectExtendedLimitInformation')
+        limitflags = jobinfo['BasicLimitInformation']['LimitFlags']
+        return bool(limitflags & JOB_OBJECT_LIMIT_BREAKAWAY_OK) or bool(limitflags & JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)
+    else:
+        return True
+
+### testing functions
+
+def parent():
+    print 'Starting parent'
+    currentProc = GetCurrentProcess()
+    if IsProcessInJob(currentProc):
+        print >> sys.stderr, "You should not be in a job object to test"
+        sys.exit(1)
+    assert CanCreateJobObject()
+    print 'File: %s' % __file__
+    command = [sys.executable, __file__, '-child']
+    print 'Running command: %s' % command
+    process = Popen(command)
+    process.kill()
+    code = process.returncode
+    print 'Child code: %s' % code
+    assert code == 127
+        
+def child():
+    print 'Starting child'
+    currentProc = GetCurrentProcess()
+    injob = IsProcessInJob(currentProc)
+    print "Is in a job?: %s" % injob
+    can_create = CanCreateJobObject()
+    print 'Can create job?: %s' % can_create
+    process = Popen('c:\\windows\\notepad.exe')
+    assert process._job
+    jobinfo = QueryInformationJobObject(process._job, 'JobObjectExtendedLimitInformation')
+    print 'Job info: %s' % jobinfo
+    limitflags = jobinfo['BasicLimitInformation']['LimitFlags']
+    print 'LimitFlags: %s' % limitflags
+    process.kill()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/mozprocess/wpk.py
@@ -0,0 +1,116 @@
+#!/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 mozprocess.
+#
+# 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>
+#  Jonathan Griffin <jgriffin@mozilla.com>
+#  Jeff Hammel <jhammel@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 ctypes import sizeof, windll, addressof, c_wchar, create_unicode_buffer
+from ctypes.wintypes import DWORD, HANDLE
+
+PROCESS_TERMINATE = 0x0001
+PROCESS_QUERY_INFORMATION = 0x0400
+PROCESS_VM_READ = 0x0010
+
+def get_pids(process_name):
+    BIG_ARRAY = DWORD * 4096
+    processes = BIG_ARRAY()
+    needed = DWORD()
+
+    pids = []
+    result = windll.psapi.EnumProcesses(processes,
+                                        sizeof(processes),
+                                        addressof(needed))
+    if not result:
+        return pids
+
+    num_results = needed.value / sizeof(DWORD)
+
+    for i in range(num_results):
+        pid = processes[i]
+        process = windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION |
+                                              PROCESS_VM_READ,
+                                              0, pid)
+        if process:
+            module = HANDLE()
+            result = windll.psapi.EnumProcessModules(process,
+                                                     addressof(module),
+                                                     sizeof(module),
+                                                     addressof(needed))
+            if result:
+                name = create_unicode_buffer(1024)
+                result = windll.psapi.GetModuleBaseNameW(process, module,
+                                                         name, len(name))
+                # TODO: This might not be the best way to
+                # match a process name; maybe use a regexp instead.
+                if name.value.startswith(process_name):
+                    pids.append(pid)
+                windll.kernel32.CloseHandle(module)
+            windll.kernel32.CloseHandle(process)
+
+    return pids
+
+def kill_pid(pid):
+    process = windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
+    if process:
+        windll.kernel32.TerminateProcess(process, 0)
+        windll.kernel32.CloseHandle(process)
+
+if __name__ == '__main__':
+    import subprocess
+    import time
+
+    # This test just opens a new notepad instance and kills it.
+
+    name = 'notepad'
+
+    old_pids = set(get_pids(name))
+    subprocess.Popen([name])
+    time.sleep(0.25)
+    new_pids = set(get_pids(name)).difference(old_pids)
+
+    if len(new_pids) != 1:
+        raise Exception('%s was not opened or get_pids() is '
+                        'malfunctioning' % name)
+
+    kill_pid(tuple(new_pids)[0])
+
+    newest_pids = set(get_pids(name)).difference(old_pids)
+
+    if len(newest_pids) != 0:
+        raise Exception('kill_pid() is malfunctioning')
+
+    print "Test passed."
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprocess/setup.py
@@ -0,0 +1,69 @@
+# ***** 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 mozprocess.
+#
+# 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>
+#  Jonathan Griffin <jgriffin@mozilla.com>
+#  Jeff Hammel <jhammel@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.1b2'
+
+# 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='mozprocess',
+      version=version,
+      description="Mozilla-authored process handling",
+      long_description=description,
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='',
+      author='Mozilla Automation and Testing Team',
+      author_email='mozmill-dev@googlegroups.com',
+      url='http://github.com/mozautomation/mozmill',
+      license='MPL',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=['mozinfo'],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/README.md
@@ -0,0 +1,80 @@
+[Mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile)
+is a python tool for creating and managing profiles for Mozilla's
+applications (Firefox, Thunderbird, etc.). In addition to creating profiles,
+mozprofile can install [addons](https://developer.mozilla.org/en/addons)
+and set [preferences](https://developer.mozilla.org/En/A_Brief_Guide_to_Mozilla_Preferences).  
+Mozprofile can be utilized from the command line or as an API.
+
+
+# Command Line Usage
+
+mozprofile may be used to create profiles, set preferences in
+profiles, or install addons into profiles.
+
+The profile to be operated on may be specified with the `--profile`
+switch. If a profile is not specified, one will be created in a
+temporary directory which will be echoed to the terminal:
+
+    (mozmill)> mozprofile 
+    /tmp/tmp4q1iEU.mozrunner
+    (mozmill)> ls /tmp/tmp4q1iEU.mozrunner
+    user.js
+
+To run mozprofile from the command line enter:
+`mozprofile --help` for a list of options.
+
+
+# API Usage
+
+To use mozprofile as an API you can import
+[mozprofile.profile](https://github.com/mozilla/mozbase/tree/master/mozprofile/mozprofile/profile.py)
+and/or the
+[AddonManager](https://github.com/mozilla/mozbase/tree/master/mozprofile/mozprofile/addons.py). 
+
+`mozprofile.profile` features a generic `Profile` class.  In addition,
+subclasses `FirefoxProfile` and `ThundebirdProfile` are available
+with preset preferences for those applications.
+
+
+# Installing Addons
+
+Addons may be installed individually or from a manifest.
+
+Example:
+
+	from mozprofile import FirefoxProfile
+	
+	# create new profile to pass to mozmill/mozrunner
+	profile = FirefoxProfile(addons=["adblock.xpi"])
+
+
+# Setting Preferences
+
+Preferences can be set in several ways:
+
+- using the API: You can pass preferences in to the Profile class's
+  constructor: `obj = FirefoxProfile(preferences=[("accessibility.typeaheadfind.flashBar", 0)])`
+- using a JSON blob file: `mozprofile --preferences myprefs.json`
+- using a `.ini` file: `mozprofile --preferences myprefs.ini`
+- via the command line: `mozprofile --pref key:value --pref key:value [...]`
+
+When setting preferences from  an `.ini` file or the `--pref` switch,
+the value will be interpolated as an integer or a boolean
+(`true`/`false`) if possible.
+
+# Setting Permissions
+
+mozprofile also takes care of adding permissions to the profile.
+See https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/permissions.py
+
+
+# Resources
+
+Other Mozilla programs offer additional and overlapping functionality
+for profiles.  There is also substantive documentation on profiles and
+their management.
+
+- [ProfileManager](https://developer.mozilla.org/en/Profile_Manager) : 
+  XULRunner application for managing profiles. Has a GUI and CLI.
+- [python-profilemanager](http://k0s.org/mozilla/hg/profilemanager/) : python CLI interface similar to ProfileManager
+- profile documentation : http://support.mozilla.com/en-US/kb/Profiles
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/__init__.py
@@ -0,0 +1,43 @@
+# ***** 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 mozprofile.
+#
+# The Initial Developer of the Original Code is
+#  The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008-2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Mikeal Rogers <mikeal.rogers@gmail.com>
+#  Clint Talbert <ctalbert@mozilla.com>
+#  Henrik Skupin <hskupin@mozilla.com>
+#  Jeff Hammel <jhammel@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 profile import *
+from addons import *
+from cli import *
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/addons.py
@@ -0,0 +1,261 @@
+# ***** 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 Mozprofile.
+#
+# 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>
+#   Mikeal Rogers <mikeal.rogers@gmail.com>
+#   Clint Talbert <ctalbert@mozilla.com>
+#   Jeff Hammel <jhammel@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
+import shutil
+import sys
+import tempfile
+import urllib2
+import zipfile
+from distutils import dir_util
+from manifestparser import ManifestParser
+from xml.dom import minidom
+
+# Needed for the AMO's rest API - https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
+AMO_API_VERSION = "1.5"
+
+class AddonManager(object):
+    """
+    Handles all operations regarding addons including: installing and cleaning addons
+    """
+
+    def __init__(self, profile):
+        """
+        profile - the path to the profile for which we install addons
+        """
+        self.profile = profile
+        self.installed_addons = []
+        # keeps track of addons and manifests that were passed to install_addons
+        self.addons = []
+        self.manifests = []
+
+
+    def install_addons(self, addons=None, manifests=None):
+        """
+        Installs all types of addons
+        addons - a list of addon paths to install
+        manifest - a list of addon manifests to install
+        """
+        # install addon paths
+        if addons:
+            if isinstance(addons, basestring):
+                addons = [addons]
+            for addon in addons:
+                self.install_from_path(addon)
+        # install addon manifests
+        if manifests:
+            if isinstance(manifests, basestring):
+                manifests = [manifests]
+            for manifest in manifests:
+                self.install_from_manifest(manifest)
+
+
+    def install_from_manifest(self, filepath):
+        """
+        Installs addons from a manifest
+        filepath - path to the manifest of addons to install
+        """
+        self.manifests.append(filepath)
+        manifest = ManifestParser()
+        manifest.read(filepath)
+        addons = manifest.get()
+
+        for addon in addons:
+            if '://' in addon['path'] or os.path.exists(addon['path']):
+                self.install_from_path(addon['path'])
+                continue
+
+            # No path specified, try to grab it off AMO
+            locale = addon.get('amo_locale', 'en_US')
+
+            query = 'https://services.addons.mozilla.org/' + locale + '/firefox/api/' + AMO_API_VERSION + '/'
+            if 'amo_id' in addon:
+                query += 'addon/' + addon['amo_id']                 # this query grabs information on the addon base on its id
+            else:
+                query += 'search/' + addon['name'] + '/default/1'   # this query grabs information on the first addon returned from a search
+            install_path = AddonManager.get_amo_install_path(query)
+            self.install_from_path(install_path)
+
+    @classmethod
+    def get_amo_install_path(self, query):
+        """
+        Return the addon xpi install path for the specified AMO query.
+        See: https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
+        for query documentation.
+        """
+        response = urllib2.urlopen(query)
+        dom = minidom.parseString(response.read())
+        for node in dom.getElementsByTagName('install')[0].childNodes:
+            if node.nodeType == node.TEXT_NODE:
+                return node.data
+
+    @classmethod
+    def addon_details(cls, addon_path):
+        """
+        returns a dictionary of details about the addon
+        - addon_path : path to the addon directory
+        Returns:
+        {'id':      u'rainbow@colors.org', # id of the addon
+         'version': u'1.4',                # version of the addon
+         'name':    u'Rainbow',            # name of the addon
+         'unpack': # whether to unpack the addon
+        """
+
+        # TODO: We don't use the unpack variable yet, but we should: bug 662683
+        details = {
+            'id': None,
+            'unpack': False,
+            'name': None,
+            'version': None
+        }
+
+        def get_namespace_id(doc, url):
+            attributes = doc.documentElement.attributes
+            namespace = ""
+            for i in range(attributes.length):
+                if attributes.item(i).value == url:
+                    if ":" in attributes.item(i).name:
+                        # If the namespace is not the default one remove 'xlmns:'
+                        namespace = attributes.item(i).name.split(':')[1] + ":"
+                        break
+            return namespace
+
+        def get_text(element):
+            """Retrieve the text value of a given node"""
+            rc = []
+            for node in element.childNodes:
+                if node.nodeType == node.TEXT_NODE:
+                    rc.append(node.data)
+            return ''.join(rc).strip()
+
+        doc = minidom.parse(os.path.join(addon_path, 'install.rdf'))
+
+        # Get the namespaces abbreviations
+        em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
+        rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
+
+        description = doc.getElementsByTagName(rdf + "Description").item(0)
+        for node in description.childNodes:
+            # Remove the namespace prefix from the tag for comparison
+            entry = node.nodeName.replace(em, "")
+            if entry in details.keys():
+                details.update({ entry: get_text(node) })
+
+        # turn unpack into a true/false value
+        if isinstance(details['unpack'], basestring):
+            details['unpack'] = details['unpack'].lower() == 'true'
+
+        return details
+
+    def install_from_path(self, path, unpack=False):
+        """
+        Installs addon from a filepath, url
+        or directory of addons in the profile.
+        - path: url, path to .xpi, or directory of addons
+        - unpack: whether to unpack unless specified otherwise in the install.rdf
+        """
+        self.addons.append(path)
+
+        # if the addon is a url, download it
+        # note that this won't work with protocols urllib2 doesn't support
+        if '://' in path:
+            response = urllib2.urlopen(path)
+            fd, path = tempfile.mkstemp(suffix='.xpi')
+            os.write(fd, response.read())
+            os.close(fd)
+            tmpfile = path
+        else:
+            tmpfile = None
+
+        # if the addon is a directory, install all addons in it
+        addons = [path]
+        if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, 'install.rdf')):
+            assert os.path.isdir(path), "Addon '%s' cannot be installed" % path
+            addons = [os.path.join(path, x) for x in os.listdir(path)]
+
+        # install each addon
+        for addon in addons:
+            tmpdir = None
+            xpifile = None
+            if addon.endswith('.xpi'):
+                tmpdir = tempfile.mkdtemp(suffix = '.' + os.path.split(addon)[-1])
+                compressed_file = zipfile.ZipFile(addon, 'r')
+                for name in compressed_file.namelist():
+                    if name.endswith('/'):
+                        os.makedirs(os.path.join(tmpdir, name))
+                    else:
+                        if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))):
+                            os.makedirs(os.path.dirname(os.path.join(tmpdir, name)))
+                        data = compressed_file.read(name)
+                        f = open(os.path.join(tmpdir, name), 'wb')
+                        f.write(data)
+                        f.close()
+                xpifile = addon
+                addon = tmpdir
+
+            # determine the addon id
+            addon_details = AddonManager.addon_details(addon)
+            addon_id = addon_details.get('id')
+            assert addon_id, 'The addon id could not be found: %s' % addon
+
+            # copy the addon to the profile
+            extensions_path = os.path.join(self.profile, 'extensions')
+            addon_path = os.path.join(extensions_path, addon_id)
+            if not unpack and not addon_details['unpack'] and xpifile:
+                if not os.path.exists(extensions_path):
+                    os.makedirs(extensions_path)
+                shutil.copy(xpifile, addon_path + '.xpi')
+            else:
+                dir_util.copy_tree(addon, addon_path, preserve_symlinks=1)
+                self.installed_addons.append(addon_path)
+
+            # remove the temporary directory, if any
+            if tmpdir:
+                dir_util.remove_tree(tmpdir)
+
+        # remove temporary file, if any
+        if tmpfile:
+            os.remove(tmpfile)
+
+    def clean_addons(self):
+        """Cleans up addons in the profile."""
+        for addon in self.installed_addons:
+            if os.path.isdir(addon):
+                dir_util.remove_tree(addon)
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/cli.py
@@ -0,0 +1,128 @@
+# ***** 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 mozprofile command line interface.
+#
+# 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):
+#   Mikeal Rogers <mikeal.rogers@gmail.com>
+#   Clint Talbert <ctalbert@mozilla.com>
+#   Jeff Hammel <jhammel@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 *****
+
+"""
+Creates and/or modifies a Firefox profile.
+The profile can be modified by passing in addons to install or preferences to set.
+If no profile is specified, a new profile is created and the path of the resulting profile is printed.
+"""
+
+import sys
+from addons import AddonManager
+from optparse import OptionParser
+from prefs import Preferences
+from profile import Profile
+
+__all__ = ['MozProfileCLI', 'cli']
+
+class MozProfileCLI(object):
+
+    module = 'mozprofile'
+
+    def __init__(self, args=sys.argv[1:]):
+        self.parser = OptionParser(description=__doc__)
+        self.add_options(self.parser)
+        (self.options, self.args) = self.parser.parse_args(args)
+
+    def add_options(self, parser):
+
+        parser.add_option("-p", "--profile", dest="profile",
+                          help="The path to the profile to operate on. If none, creates a new profile in temp directory")
+        parser.add_option("-a", "--addon", dest="addons",
+                          action="append", default=[],
+                          help="Addon paths to install. Can be a filepath, a directory containing addons, or a url")
+        parser.add_option("--addon-manifests", dest="addon_manifests",
+                          action="append",
+                          help="An addon manifest to install")
+        parser.add_option("--pref", dest="prefs",
+                          action='append', default=[],
+                          help="A preference to set. Must be a key-value pair separated by a ':'")
+        parser.add_option("--preferences", dest="prefs_files",
+                          action='append', default=[],
+                          metavar="FILE",
+                          help="read preferences from a JSON or INI file. For INI, use 'file.ini:section' to specify a particular section.")
+
+    def profile_args(self):
+        """arguments to instantiate the profile class"""
+        return dict(profile=self.options.profile,
+                    addons=self.options.addons,
+                    addon_manifests=self.options.addon_manifests,
+                    preferences=self.preferences())
+
+    def preferences(self):
+        """profile preferences"""
+
+        # object to hold preferences
+        prefs = Preferences()
+
+        # add preferences files
+        for prefs_file in self.options.prefs_files:
+            prefs.add_file(prefs_file)
+
+        # change CLI preferences into 2-tuples
+        separator = ':'
+        cli_prefs = []
+        for pref in self.options.prefs:
+            if separator not in pref:
+                self.parser.error("Preference must be a key-value pair separated by a ':' (You gave: %s)" % pref)
+            cli_prefs.append(pref.split(separator, 1))
+
+        # string preferences
+        prefs.add(cli_prefs, cast=True)
+
+        return prefs()
+
+
+def cli(args=sys.argv[1:]):
+
+    # process the command line
+    cli = MozProfileCLI(args)
+
+    # create the profile
+    kwargs = cli.profile_args()
+    kwargs['restore'] = False
+    profile = Profile(**kwargs)
+
+    # if no profile was passed in print the newly created profile
+    if not cli.options.profile:
+        print profile.profile
+
+if __name__ == '__main__':
+    cli()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/permissions.py
@@ -0,0 +1,316 @@
+# ***** 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 Mozprofile.
+#
+# 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 *****
+
+
+"""
+add permissions to the profile
+"""
+
+__all__ = ['LocationsSyntaxError', 'Location', 'PermissionsManager']
+
+import codecs
+import itertools
+import os
+import sqlite3
+import urlparse
+
+class LocationsSyntaxError(Exception):
+    "Signifies a syntax error on a particular line in server-locations.txt."
+
+    def __init__(self, lineno, msg = None):
+        self.lineno = lineno
+        self.msg = msg
+
+    def __str__(self):
+        s = "Syntax error on line %s" % self.lineno
+        if self.msg:
+            s += ": %s." % self.msg
+        else:
+            s += "."
+        return s
+
+
+class Location(object):
+    "Represents a location line in server-locations.txt."
+
+    attrs = ('scheme', 'host', 'port')
+
+    def __init__(self, scheme, host, port, options):
+        for attr in self.attrs:
+            setattr(self, attr, locals()[attr])
+        self.options = options
+
+    def isEqual(self, location):
+        "compare scheme://host:port, but ignore options"
+        return len([i for i in self.attrs if getattr(self, i) == getattr(location, i)]) == len(self.attrs)
+
+    __eq__ = isEqual
+
+    def url(self):
+        return '%s://%s:%s' % (self.scheme, self.host, self.port)
+
+    def __str__(self):
+        return  '%s  %s' % (self.url(), ','.join(self.options))
+
+
+class PermissionsManager(object):
+    _num_permissions = 0
+
+    def __init__(self, profileDir, locations=None):
+        self._profileDir = profileDir
+        self._locations = [] # for cleanup
+        if locations:
+            if isinstance(locations, list):
+                for l in locations:
+                    self.add_host(**l)
+            elif isinstance(locations, dict):
+                self.add_host(**locations)
+            elif os.path.exists(locations):
+                self.add_file(locations)
+
+    def write_permission(self, location):
+        """write permissions to the sqlite database"""
+
+        # Open database and create table
+        permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sqlite"))
+        cursor = permDB.cursor();
+        # SQL copied from
+        # http://mxr.mozilla.org/mozilla-central/source/extensions/cookie/nsPermissionManager.cpp
+        cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
+           id INTEGER PRIMARY KEY,
+           host TEXT,
+           type TEXT,
+           permission INTEGER,
+           expireType INTEGER,
+           expireTime INTEGER)""")
+
+        # set the permissions
+        permissions = {'allowXULXBL':[(location.host, 'noxul' not in location.options)]}
+        for perm in permissions.keys():
+            for host,allow in permissions[perm]:
+                self._num_permissions += 1
+                cursor.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0)",
+                               (self._num_permissions, host, perm, 1 if allow else 2))
+
+        # Commit and close
+        permDB.commit()
+        cursor.close()
+
+    def add(self, *newLocations):
+        """add locations to the database"""
+
+        for location in newLocations:
+            for loc in self._locations:
+                if loc.isEqual(location):
+                    print >> sys.stderr, "Duplicate location: %s" % location.url()
+                    break
+        else:
+            self._locations.append(location)
+            self.write_permission(location)
+
+    def add_host(self, host, port='80', scheme='http', options='privileged'):
+        if isinstance(options, basestring):
+            options = options.split(',')
+        self.add(Location(scheme, host, port, options))
+
+    def add_file(self, path):
+        """add permissions from a locations file """
+        self.add(self.read_locations(path))
+
+    def read_locations(self, filename):
+        """
+        Reads the file (in the format of server-locations.txt) and add all
+        valid locations to the self.locations array.
+
+        This format:
+        http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt
+        """
+
+        locationFile = codecs.open(filename, "r", "UTF-8")
+
+        locations = []
+        lineno = 0
+        seenPrimary = False
+        for line in locationFile:
+            line = line.strip()
+            lineno += 1
+
+            # check for comments and blank lines
+            if line.startswith("#") or not line:
+                continue
+
+            # split the server from the options
+            try:
+                server, options = line.rsplit(None, 1)
+                options = options.split(',')
+            except ValueError:
+                server = line
+                options = []
+
+            # parse the server url
+            if '://' not in server:
+                server = 'http://' + server
+            scheme, netloc, path, query, fragment = urlparse.urlsplit(server)
+            # get the host and port
+            try:
+                host, port = netloc.rsplit(':', 1)
+            except ValueError:
+                host = netloc
+                port = '80'
+            try:
+                int(port)
+            except ValueError:
+                raise LocationsSyntaxError(lineno, 'bad value for port: %s' % line)
+
+            # check for primary location
+            if "primary" in options:
+                if seenPrimary:
+                    raise LocationsSyntaxError(lineno, "multiple primary locations")
+                seenPrimary = True
+
+            # add the location
+            locations.append(Location(scheme, host, port, options))
+
+        # ensure that a primary is found
+        if not seenPrimary:
+            raise LocationsSyntaxError(lineno + 1, "missing primary location")
+
+        return locations
+
+    def getNetworkPreferences(self, proxy=False):
+        """
+        take known locations and generate preferences to handle permissions and proxy
+        returns a tuple of prefs, user_prefs
+        """
+
+        # Grant God-power to all the privileged servers on which tests run.
+        prefs = []
+        privileged = filter(lambda loc: "privileged" in loc.options, self._locations)
+        for (i, l) in itertools.izip(itertools.count(1), privileged):
+            prefs.append(("capability.principal.codebase.p%s.granted" % i, "UniversalPreferencesWrite UniversalXPConnect UniversalPreferencesRead"))
+
+            # TODO: do we need the port?
+            prefs.append(("capability.principal.codebase.p%s.id" % i, l.scheme + "://" + l.host))
+            prefs.append(("capability.principal.codebase.p%s.subjectName" % i, ""))
+
+        if proxy:
+            user_prefs = self.pacPrefs()
+        else:
+            user_prefs = []
+
+        return prefs, user_prefs
+
+    def pacPrefs(self):
+        """
+        return preferences for Proxy Auto Config. originally taken from
+        http://mxr.mozilla.org/mozilla-central/source/build/automation.py.in
+        """
+
+        prefs = []
+
+        # We need to proxy every server but the primary one.
+        origins = ["'%s'" % l.url()
+                   for l in self._locations
+                   if "primary" not in l.options]
+        origins = ", ".join(origins)
+
+        # TODO: this is not a reliable way to determine the Proxy host
+        for l in self._locations:
+            if "primary" in l.options:
+                webServer = l.host
+                httpPort  = l.port
+                sslPort   = 443
+
+        # TODO: this should live in a template!
+        pacURL = """data:text/plain,
+function FindProxyForURL(url, host)
+{
+  var origins = [%(origins)s];
+  var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
+                         '://' +
+                         '(?:[^/@]*@)?' +
+                         '(.*?)' +
+                         '(?::(\\\\\\\\d+))?/');
+  var matches = regex.exec(url);
+  if (!matches)
+    return 'DIRECT';
+  var isHttp = matches[1] == 'http';
+  var isHttps = matches[1] == 'https';
+  var isWebSocket = matches[1] == 'ws';
+  var isWebSocketSSL = matches[1] == 'wss';
+  if (!matches[3])
+  {
+    if (isHttp | isWebSocket) matches[3] = '80';
+    if (isHttps | isWebSocketSSL) matches[3] = '443';
+  }
+  if (isWebSocket)
+    matches[1] = 'http';
+  if (isWebSocketSSL)
+    matches[1] = 'https';
+
+  var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
+  if (origins.indexOf(origin) < 0)
+    return 'DIRECT';
+  if (isHttp)
+    return 'PROXY %(remote)s:%(httpport)s';
+  if (isHttps || isWebSocket || isWebSocketSSL)
+    return 'PROXY %(remote)s:%(sslport)s';
+  return 'DIRECT';
+}""" % { "origins": origins,
+         "remote":  webServer,
+         "httpport":httpPort,
+         "sslport": sslPort }
+        pacURL = "".join(pacURL.splitlines())
+
+        prefs.append(("network.proxy.type", 2))
+        prefs.append(("network.proxy.autoconfig_url", pacURL))
+
+        return prefs
+
+    def clean_permissions(self):
+        """Removed permissions added by mozprofile."""
+
+        # Open database and create table
+        permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sqlite"))
+        cursor = permDB.cursor();
+
+        # TODO: only delete values that we add, this would require sending in the full permissions object
+        cursor.execute("DROP TABLE IF EXISTS moz_hosts");
+
+        # Commit and close
+        permDB.commit()
+        cursor.close()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/prefs.py
@@ -0,0 +1,249 @@
+#!/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 mozprofile.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Clint Talbert <ctalbert@mozilla.com>
+#   Jeff Hammel <jhammel@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 *****
+
+"""
+user preferences
+"""
+
+import os
+import re
+from ConfigParser import SafeConfigParser as ConfigParser
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+class PreferencesReadError(Exception):
+    """read error for prefrences files"""
+
+
+class Preferences(object):
+    """assembly of preferences from various sources"""
+
+    def __init__(self, prefs=None):
+        self._prefs = []
+        if prefs:
+            self.add(prefs)
+
+    def add(self, prefs, cast=False):
+        """
+        - cast: whether to cast strings to value, e.g. '1' -> 1
+        """
+        # wants a list of 2-tuples
+        if isinstance(prefs, dict):
+            prefs = prefs.items()
+        if cast:
+            prefs = [(i, self.cast(j)) for i, j in prefs]
+        self._prefs += prefs
+
+    def add_file(self, path):
+        """a preferences from a file"""
+        self.add(self.read(path))
+
+    def __call__(self):
+        return self._prefs
+
+    @classmethod
+    def cast(cls, value):
+        """
+        interpolate a preference from a string
+        from the command line or from e.g. an .ini file, there is no good way to denote
+        what type the preference value is, as natively it is a string
+        - integers will get cast to integers
+        - true/false will get cast to True/False
+        - anything enclosed in single quotes will be treated as a string with the ''s removed from both sides
+        """
+
+        if not isinstance(value, basestring):
+            return value # no op
+        quote = "'"
+        if value == 'true':
+            return  True
+        if value == 'false':
+            return False
+        try:
+            return int(value)
+        except ValueError:
+            pass
+        if value.startswith(quote) and value.endswith(quote):
+            value = value[1:-1]
+        return value
+
+
+    @classmethod
+    def read(cls, path):
+        """read preferences from a file"""
+
+        section = None # for .ini files
+        basename = os.path.basename(path)
+        if ':' in basename:
+            # section of INI file
+            path, section = path.rsplit(':', 1)
+
+        if not os.path.exists(path):
+            raise PreferencesReadError("'%s' does not exist" % path)
+
+        if section:
+            try:
+                return cls.read_ini(path, section)
+            except PreferencesReadError:
+                raise
+            except Exception, e:
+                raise PreferencesReadError(str(e))
+
+        # try both JSON and .ini format
+        try:
+            return cls.read_json(path)
+        except Exception, e:
+            try:
+                return cls.read_ini(path)
+            except Exception, f:
+                for exception in e, f:
+                    if isinstance(exception, PreferencesReadError):
+                        raise exception
+                raise PreferencesReadError("Could not recognize format of %s" % path)
+
+
+    @classmethod
+    def read_ini(cls, path, section=None):
+        """read preferences from an .ini file"""
+
+        parser = ConfigParser()
+        parser.read(path)
+
+        if section:
+            if section not in parser.sections():
+                raise PreferencesReadError("No section '%s' in %s" % (section, path))
+            retval = parser.items(section, raw=True)
+        else:
+            retval = parser.defaults().items()
+
+        # cast the preferences since .ini is just strings
+        return [(i, cls.cast(j)) for i, j in retval]
+
+    @classmethod
+    def read_json(cls, path):
+        """read preferences from a JSON blob"""
+
+        prefs = json.loads(file(path).read())
+
+        if type(prefs) not in [list, dict]:
+            raise PreferencesReadError("Malformed preferences: %s" % path)
+        if isinstance(prefs, list):
+            if [i for i in prefs if type(i) != list or len(i) != 2]:
+                raise PreferencesReadError("Malformed preferences: %s" % path)
+            values = [i[1] for i in prefs]
+        elif isinstance(prefs, dict):
+            values = prefs.values()
+        else:
+            raise PreferencesReadError("Malformed preferences: %s" % path)
+        types = (bool, basestring, int)
+        if [i for i in values
+            if not [isinstance(i, j) for j in types]]:
+            raise PreferencesReadError("Only bool, string, and int values allowed")
+        return prefs
+
+    @classmethod
+    def read_prefs(cls, path, pref_setter='user_pref'):
+        """read preferences from (e.g.) prefs.js"""
+
+        comment = re.compile('/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/', re.MULTILINE)
+
+        token = '##//' # magical token
+        lines = [i.strip() for i in file(path).readlines() if i.strip()]
+        _lines = []
+        for line in lines:
+            if line.startswith('#'):
+                continue
+            if '//' in line:
+                line = line.replace('//', token)
+            _lines.append(line)
+        string = '\n'.join(_lines)
+        string = re.sub(comment, '', string)
+
+        retval = []
+        def pref(a, b):
+            retval.append((a, b))
+        lines = [i.strip().rstrip(';') for i in string.split('\n') if i.strip()]
+
+        _globals = {'retval': retval, 'true': True, 'false': False}
+        _globals[pref_setter] = pref
+        for line in lines:
+            try:
+                eval(line, _globals, {})
+            except SyntaxError:
+                print line
+                raise
+
+        # de-magic the token
+        for index, (key, value) in enumerate(retval):
+            if isinstance(value, basestring) and token in value:
+                retval[index] = (key, value.replace(token, '//'))
+
+        return retval
+
+    @classmethod
+    def write(_file, prefs, pref_string='user_pref("%s", %s);'):
+        """write preferences to a file"""
+
+        if isinstance(_file, basestring):
+            f = file(_file, 'w')
+        else:
+            f = _file
+
+        if isinstance(prefs, dict):
+            prefs = prefs.items()
+
+        for key, value in prefs:
+            if value is True:
+                print >> f, pref_string % (key, 'true')
+            elif value is False:
+                print >> f, pref_string % (key, 'false')
+            elif isinstance(value, basestring):
+                print >> f, pref_string % (key, repr(string(value)))
+            else:
+                print >> f, pref_string % (key, value) # should be numeric!
+
+        if isinstance(_file, basestring):
+            f.close()
+
+if __name__ == '__main__':
+    pass
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/profile.py
@@ -0,0 +1,272 @@
+# ***** 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 mozprofile.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008-2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Mikeal Rogers <mikeal.rogers@gmail.com>
+#   Clint Talbert <ctalbert@mozilla.com>
+#   Henrik Skupin <hskupin@mozilla.com>
+#   Jeff Hammel <jhammel@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 *****
+
+__all__ = ['Profile', 'FirefoxProfile', 'ThunderbirdProfile']
+
+import os
+import tempfile
+from addons import AddonManager
+from permissions import PermissionsManager
+from shutil import rmtree
+
+try:
+    import simplejson
+except ImportError:
+    import json as simplejson
+
+class Profile(object):
+    """Handles all operations regarding profile. Created new profiles, installs extensions,
+    sets preferences and handles cleanup."""
+
+    def __init__(self, profile=None, addons=None, addon_manifests=None, preferences=None, locations=None, proxy=False, restore=True):
+
+        # if true, remove installed addons/prefs afterwards
+        self.restore = restore
+
+        # Handle profile creation
+        self.create_new = not profile
+        if profile:
+            # Ensure we have a full path to the profile
+            self.profile = os.path.abspath(os.path.expanduser(profile))
+            if not os.path.exists(self.profile):
+                os.makedirs(self.profile)
+        else:
+            self.profile = self.create_new_profile()
+
+        # set preferences
+        if hasattr(self.__class__, 'preferences'):
+            # class preferences
+            self.set_preferences(self.__class__.preferences)
+        self._preferences = preferences
+        if preferences:
+            # supplied preferences
+            if isinstance(preferences, dict):
+                # unordered
+                preferences = preferences.items()
+            # sanity check
+            assert not [i for i in preferences
+                        if len(i) != 2]
+        else:
+            preferences = []
+        self.set_preferences(preferences)
+
+        # set permissions
+        self._locations = locations # store this for reconstruction
+        self._proxy = proxy
+        self.permission_manager = PermissionsManager(self.profile, locations)
+        prefs_js, user_js = self.permission_manager.getNetworkPreferences(proxy)
+        self.set_preferences(prefs_js, 'prefs.js')
+        self.set_preferences(user_js)
+
+        # handle addon installation
+        self.addon_manager = AddonManager(self.profile)
+        self.addon_manager.install_addons(addons, addon_manifests)
+
+    def exists(self):
+        """returns whether the profile exists or not"""
+        return os.path.exists(self.profile)
+
+    def reset(self):
+        """
+        reset the profile to the beginning state
+        """
+        self.cleanup()
+        if self.create_new:
+            profile = None
+        else:
+            profile = self.profile
+        self.__init__(profile=profile,
+                      addons=self.addon_manager.addons,
+                      addon_manifests=self.addon_manager.manifests,
+                      preferences=self._preferences,
+                      locations=self._locations,
+                      proxy = self._proxy)
+
+    def create_new_profile(self):
+        """Create a new clean profile in tmp which is a simple empty folder"""
+        profile = tempfile.mkdtemp(suffix='.mozrunner')
+        return profile
+
+
+    ### methods for preferences
+
+    def set_preferences(self, preferences, filename='user.js'):
+        """Adds preferences dict to profile preferences"""
+
+        # append to the file
+        prefs_file = os.path.join(self.profile, filename)
+        f = open(prefs_file, 'a')
+
+        if isinstance(preferences, dict):
+            # order doesn't matter
+            preferences = preferences.items()
+
+        # write the preferences
+        if preferences:
+            f.write('\n#MozRunner Prefs Start\n')
+            _prefs = [(simplejson.dumps(k), simplejson.dumps(v) )
+                      for k, v in preferences]
+            for _pref in _prefs:
+                f.write('user_pref(%s, %s);\n' % _pref)
+            f.write('#MozRunner Prefs End\n')
+        f.close()
+
+    def pop_preferences(self):
+        """
+        pop the last set of preferences added
+        returns True if popped
+        """
+
+        # our magic markers
+        delimeters = ('#MozRunner Prefs Start', '#MozRunner Prefs End')
+
+        lines = file(os.path.join(self.profile, 'user.js')).read().splitlines()
+        def last_index(_list, value):
+            """
+            returns the last index of an item;
+            this should actually be part of python code but it isn't
+            """
+            for index in reversed(range(len(_list))):
+                if _list[index] == value:
+                    return index
+        s = last_index(lines, delimeters[0])
+        e = last_index(lines, delimeters[1])
+
+        # ensure both markers are found
+        if s is None:
+            assert e is None, '%s found without %s' % (delimeters[1], delimeters[0])
+            return False # no preferences found
+        elif e is None:
+            assert e is None, '%s found without %s' % (delimeters[0], delimeters[1])
+
+        # ensure the markers are in the proper order
+        assert e > s, '%s found at %s, while %s found at %s' (delimeter[1], e, delimeter[0], s)
+
+        # write the prefs
+        cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:])
+        f = file(os.path.join(self.profile, 'user.js'), 'w')
+        return True
+
+    def clean_preferences(self):
+        """Removed preferences added by mozrunner."""
+        while True:
+            if not self.pop_preferences():
+                break
+
+    ### cleanup
+
+    def _cleanup_error(self, function, path, excinfo):
+        """ Specifically for windows we need to handle the case where the windows
+            process has not yet relinquished handles on files, so we do a wait/try
+            construct and timeout if we can't get a clear road to deletion
+        """
+        try:
+            from exceptions import WindowsError
+            from time import sleep
+            def is_file_locked():
+                return excinfo[0] is WindowsError and excinfo[1].winerror == 32
+
+            if excinfo[0] is WindowsError and excinfo[1].winerror == 32:
+                # Then we're on windows, wait to see if the file gets unlocked
+                # we wait 10s
+                count = 0
+                while count < 10:
+                    sleep(1)
+                    try:
+                        function(path)
+                        break
+                    except:
+                        count += 1
+        except ImportError:
+            # We can't re-raise an error, so we'll hope the stuff above us will throw
+            pass
+
+
+    def cleanup(self):
+        """Cleanup operations on the profile."""
+        if self.restore:
+            if self.create_new:
+                if os.path.exists(self.profile):
+                    rmtree(self.profile, onerror=self._cleanup_error)
+            else:
+                self.clean_preferences()
+                self.addon_manager.clean_addons()
+                self.permission_manager.clean_permissions()
+
+    __del__ = cleanup
+
+class FirefoxProfile(Profile):
+    """Specialized Profile subclass for Firefox"""
+    preferences = {# Don't automatically update the application
+                   'app.update.enabled' : False,
+                   # Don't restore the last open set of tabs if the browser has crashed
+                   'browser.sessionstore.resume_from_crash': False,
+                   # Don't check for the default web browser
+                   'browser.shell.checkDefaultBrowser' : False,
+                   # Don't warn on exit when multiple tabs are open
+                   'browser.tabs.warnOnClose' : False,
+                   # Don't warn when exiting the browser
+                   'browser.warnOnQuit': False,
+                   # Only install add-ons from the profile and the application scope
+                   # Also ensure that those are not getting disabled.
+                   # see: https://developer.mozilla.org/en/Installing_extensions
+                   'extensions.enabledScopes' : 5,
+                   'extensions.autoDisableScopes' : 10,
+                   # Don't install distribution add-ons from the app folder
+                   'extensions.installDistroAddons' : False,
+                   # Dont' run the add-on compatibility check during start-up
+                   'extensions.showMismatchUI' : False,
+                   # Don't automatically update add-ons
+                   'extensions.update.enabled'    : False,
+                   # Don't open a dialog to show available add-on updates
+                   'extensions.update.notifyUser' : False,
+                   }
+
+class ThunderbirdProfile(Profile):
+    preferences = {'extensions.update.enabled'    : False,
+                   'extensions.update.notifyUser' : False,
+                   'browser.shell.checkDefaultBrowser' : False,
+                   'browser.tabs.warnOnClose' : False,
+                   'browser.warnOnQuit': False,
+                   'browser.sessionstore.resume_from_crash': False,
+                   # prevents the 'new e-mail address' wizard on new profile
+                   'mail.provider.enabled': False,
+                   }
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozprofile/setup.py
@@ -0,0 +1,83 @@
+# ***** 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 mozprofile.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Mikeal Rogers <mikeal.rogers@gmail.com>
+#   Clint Talbert <ctalbert@mozilla.com>
+#   Jeff Hammel <jhammel@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 *****
+
+import os
+import sys
+from setuptools import setup, find_packages
+
+version = '0.1b2'
+
+# we only support python 2 right now
+assert sys.version_info[0] == 2
+
+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__))
+try:
+    description = file(os.path.join(here, 'README.md')).read()
+except (OSError, IOError):
+    description = ''
+
+setup(name='mozprofile',
+      version=version,
+      description="handling of Mozilla XUL app profiles",
+      long_description=description,
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='',
+      author='Mozilla Automation + Testing Team',
+      author_email='mozmill-dev@googlegroups.com',
+      url='http://github.com/mozautomation/mozmill',
+      license='MPL',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=deps,
+      entry_points="""
+      # -*- Entry points: -*-
+      [console_scripts]
+      mozprofile = mozprofile:cli
+      """,
+      )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/README.md
@@ -0,0 +1,43 @@
+[mozrunner](https://github.com/mozilla/mozbase/tree/master/mozrunner)
+is a [python package](http://pypi.python.org/pypi/mozrunner)
+which handles running of Mozilla applications.
+mozrunner utilizes [mozprofile](/en/Mozprofile)
+for managing application profiles
+and [mozprocess](/en/Mozprocess) for robust process control.
+
+mozrunner may be used from the command line or programmatically as an API.
+
+
+# Command Line Usage
+
+The `mozrunner` command will launch the application (specified by
+`--app`) from a binary specified with `-b` or as located on the `PATH`.
+
+mozrunner takes the command line options from 
+[mozprofile](/en/Mozprofile) for constructing the profile to be used by 
+the application.
+
+Run `mozrunner --help` for detailed information on the command line
+program.
+
+
+# API Usage
+
+mozrunner features a base class, 
+[mozrunner.runner.Runner](https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py) 
+which is an integration layer API for interfacing with Mozilla applications.
+
+mozrunner also exposes two application specific classes,
+`FirefoxRunner` and `ThunderbirdRunner` which record the binary names
+necessary for the `Runner` class to find them on the system.
+
+Example API usage:
+
+    from mozrunner import FirefoxRunner
+	
+    # start Firefox on a new profile
+    runner = FirefoxRunner()
+    runner.start()
+
+See also a comparable implementation for [selenium](http://seleniumhq.org/): 
+http://code.google.com/p/selenium/source/browse/trunk/py/selenium/webdriver/firefox/firefox_binary.py
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/__init__.py
@@ -0,0 +1,40 @@
+# ***** 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 mozrunner.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008-2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Mikeal Rogers <mikeal.rogers@gmail.com>
+#  Clint Talbert <ctalbert@mozilla.com>
+#  Henrik Skupin <hskupin@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 runner import *
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/runner.py
@@ -0,0 +1,436 @@
+# ***** 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 mozrunner.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008-2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Mikeal Rogers <mikeal.rogers@gmail.com>
+#   Clint Talbert <ctalbert@mozilla.com>
+#   Henrik Skupin <hskupin@mozilla.com>
+#   Jeff Hammel <jhammel@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 *****
+
+__all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'runners', 'CLI', 'cli', 'package_metadata']
+
+import mozinfo
+import optparse
+import os
+import sys
+import ConfigParser
+
+from utils import get_metadata_from_egg
+from utils import findInPath
+from mozprofile import *
+from mozprocess.processhandler import ProcessHandler
+
+package_metadata = get_metadata_from_egg('mozrunner')
+
+class BinaryLocationException(Exception):
+    """exception for failure to find the binary"""
+
+
+class Runner(object):
+    """Handles all running operations. Finds bins, runs and kills the process."""
+
+    ### data to be filled in by subclasses
+    profile = Profile # profile class to use by default
+    names = [] # names of application to look for on PATH
+    app_name = '' # name of application in windows registry
+    program_names = [] # names of application in windows program files
+
+    @classmethod
+    def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None,
+                                               clean_profile=True, process_class=ProcessHandler):
+        profile = cls.profile_class(**(profile_args or {}))
+        return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs,
+                                           clean_profile=clean_profile, process_class=process_class)
+
+    def __init__(self, profile, binary=None, cmdargs=None, env=None,
+                 kp_kwargs=None, clean_profile=True, process_class=ProcessHandler):
+        self.process_handler = None
+        self.process_class = process_class
+        self.profile = profile
+        self.clean_profile = clean_profile
+
+        self.firstrun = False
+
+        # find the binary
+        self.binary = self.__class__.get_binary(binary)
+        if not os.path.exists(self.binary):
+            raise OSError("Binary path does not exist: %s" % self.binary)
+
+        self.cmdargs = cmdargs or []
+        _cmdargs = [i for i in self.cmdargs
+                    if i != '-foreground']
+        if len(_cmdargs) != len(self.cmdargs):
+            # foreground should be last; see
+            # - https://bugzilla.mozilla.org/show_bug.cgi?id=625614
+            # - https://bugzilla.mozilla.org/show_bug.cgi?id=626826
+            self.cmdargs = _cmdargs
+            self.cmdargs.append('-foreground')
+
+        # process environment
+        if env is None:
+            self.env = os.environ.copy()
+        else:
+            self.env = env.copy()
+        # allows you to run an instance of Firefox separately from any other instances
+        self.env['MOZ_NO_REMOTE'] = '1'
+        # keeps Firefox attached to the terminal window after it starts
+        self.env['NO_EM_RESTART'] = '1'
+
+        # set the library path if needed on linux
+        if sys.platform == 'linux2' and self.binary.endswith('-bin'):
+            dirname = os.path.dirname(self.binary)
+            if os.environ.get('LD_LIBRARY_PATH', None):
+                self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
+            else:
+                self.env['LD_LIBRARY_PATH'] = dirname
+
+        # arguments for ProfessHandler.Process
+        self.kp_kwargs = kp_kwargs or {}
+
+    @classmethod
+    def get_binary(cls, binary=None):
+        """determine the binary"""
+        if binary is None:
+            binary = cls.find_binary()
+            if binary is None:
+                raise BinaryLocationException("Your binary could not be located; you will need to set it")
+            return binary
+        elif mozinfo.isMac and binary.find('Contents/MacOS/') == -1:
+            return os.path.join(binary, 'Contents/MacOS/%s-bin' % cls.names[0])
+        else:
+            return binary
+
+    @classmethod
+    def find_binary(cls):
+        """Finds the binary for class names if one was not provided."""
+
+        binary = None
+        if mozinfo.isUnix:
+            for name in cls.names:
+                binary = findInPath(name)
+                if binary:
+                    return binary
+        elif mozinfo.isWin:
+
+            # find the default executable from the windows registry
+            try:
+                # assumes cls.app_name is defined, as it should be for implementors
+                import _winreg
+                app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r"Software\Mozilla\Mozilla %s" % cls.app_name)
+                version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion")
+                version_key = _winreg.OpenKey(app_key, version + r"\Main")
+                path, _ = _winreg.QueryValueEx(version_key, "PathToExe")
+                return path
+            except: # XXX not sure what type of exception this should be
+                pass
+
+            # search for the binary in the path
+            for name in cls.names:
+                binary = findInPath(name)
+                if binary:
+                    return binary
+
+            # search for the binary in program files
+            if sys.platform == 'cygwin':
+                program_files = os.environ['PROGRAMFILES']
+            else:
+                program_files = os.environ['ProgramFiles']
+
+            program_files = [program_files]
+            if  "ProgramFiles(x86)" in os.environ:
+                program_files.append(os.environ["ProgramFiles(x86)"])
+            for program_file in program_files:
+                for program_name in cls.program_names:
+                    path = os.path.join(program_name, program_file, 'firefox.exe')
+                    if os.path.isfile(path):
+                        return path
+
+        elif mozinfo.isMac:
+            for name in cls.names:
+                appdir = os.path.join('Applications', name.capitalize()+'.app')
+                if os.path.isdir(os.path.join(os.path.expanduser('~/'), appdir)):
+                    binary = os.path.join(os.path.expanduser('~/'), appdir,
+                                          'Contents/MacOS/'+name+'-bin')
+                elif os.path.isdir('/'+appdir):
+                    binary = os.path.join("/"+appdir, 'Contents/MacOS/'+name+'-bin')
+
+                if binary is not None:
+                    if not os.path.isfile(binary):
+                        binary = binary.replace(name+'-bin', 'firefox-bin')
+                    if not os.path.isfile(binary):
+                        binary = None
+                if binary:
+                    return binary
+        return binary
+
+    @property
+    def command(self):
+        """Returns the command list to run."""
+        return [self.binary, '-profile', self.profile.profile]
+
+    def get_repositoryInfo(self):
+        """Read repository information from application.ini and platform.ini."""
+
+        config = ConfigParser.RawConfigParser()
+        dirname = os.path.dirname(self.binary)
+        repository = { }
+
+        for file, section in [('application', 'App'), ('platform', 'Build')]:
+            config.read(os.path.join(dirname, '%s.ini' % file))
+
+            for key, id in [('SourceRepository', 'repository'),
+                            ('SourceStamp', 'changeset')]:
+                try:
+                    repository['%s_%s' % (file, id)] = config.get(section, key);
+                except:
+                    repository['%s_%s' % (file, id)] = None
+
+        return repository
+
+    def is_running(self):
+        return self.process_handler is not None
+
+    def start(self):
+        """Run self.command in the proper environment."""
+
+        # ensure you are stopped
+        self.stop()
+
+        # ensure the profile exists
+        if not self.profile.exists():
+            self.profile.reset()
+            self.firstrun = False
+
+        # run once to register any extensions
+        # see:
+        # - http://hg.mozilla.org/releases/mozilla-1.9.2/file/915a35e15cde/build/automation.py.in#l702
+        # - http://mozilla-xp.com/mozilla.dev.apps.firefox/Rules-for-when-firefox-bin-restarts-it-s-process
+        # This run just calls through processhandler to popen directly as we
+        # are not particuarly cared in tracking this process
+        if not self.firstrun:
+            firstrun = ProcessHandler.Process(self.command+['-silent', '-foreground'], env=self.env, **self.kp_kwargs)
+            firstrun.wait()
+            self.firstrun = True
+
+        # now run for real, this run uses the managed processhandler
+        self.process_handler = self.process_class(self.command+self.cmdargs, env=self.env, **self.kp_kwargs)
+        self.process_handler.run()
+
+    def wait(self, timeout=None, outputTimeout=None):
+        """Wait for the app to exit."""
+        if self.process_handler is None:
+            return
+        self.process_handler.waitForFinish(timeout=timeout, outputTimeout=outputTimeout)
+        self.process_handler = None
+
+    def stop(self):
+        """Kill the app"""
+        if self.process_handler is None:
+            return
+        self.process_handler.kill()
+        self.process_handler = None
+
+    def reset(self):
+        """
+        reset the runner between runs
+        currently, only resets the profile, but probably should do more
+        """
+        self.profile.reset()
+
+    def cleanup(self):
+        self.stop()
+        if self.clean_profile:
+            self.profile.cleanup()
+
+    __del__ = cleanup
+
+
+class FirefoxRunner(Runner):
+    """Specialized Runner subclass for running Firefox."""
+
+    app_name = 'Firefox'
+    profile_class = FirefoxProfile
+    program_names = ['Mozilla Firefox']
+
+    # (platform-dependent) names of binary
+    if mozinfo.isMac:
+        names = ['firefox', 'minefield', 'shiretoko']
+    elif mozinfo.isUnix:
+        names = ['firefox', 'mozilla-firefox', 'iceweasel']
+    elif mozinfo.isWin:
+        names =['firefox']
+    else:
+        raise AssertionError("I don't know what platform you're on")
+
+    def __init__(self, profile, **kwargs):
+        Runner.__init__(self, profile, **kwargs)
+
+        # Find application version number
+        appdir = os.path.dirname(os.path.realpath(self.binary))
+        appini = ConfigParser.RawConfigParser()
+        appini.read(os.path.join(appdir, 'application.ini'))
+        # Version needs to be of the form 3.6 or 4.0b and not the whole string
+        version = appini.get('App', 'Version').rstrip('0123456789pre').rstrip('.')
+
+        # Disable compatibility check. See:
+        # - http://kb.mozillazine.org/Extensions.checkCompatibility
+        # - https://bugzilla.mozilla.org/show_bug.cgi?id=659048
+        preference = {'extensions.checkCompatibility.' + version: False,
+                      'extensions.checkCompatibility.nightly': False}
+        self.profile.set_preferences(preference)
+
+    @classmethod
+    def get_binary(cls, binary=None):
+        if (not binary) and 'BROWSER_PATH' in os.environ:
+            return os.environ['BROWSER_PATH']
+        return Runner.get_binary(binary)
+
+class ThunderbirdRunner(Runner):
+    """Specialized Runner subclass for running Thunderbird"""
+    app_name = 'Thunderbird'
+    profile_class = ThunderbirdProfile
+    names = ["thunderbird", "shredder"]
+
+runners = {'firefox': FirefoxRunner,
+           'thunderbird': ThunderbirdRunner}
+
+class CLI(MozProfileCLI):
+    """Command line interface."""
+
+    module = "mozrunner"
+
+    def __init__(self, args=sys.argv[1:]):
+        """
+        Setup command line parser and parse arguments
+        - args : command line arguments
+        """
+
+        self.metadata = getattr(sys.modules[self.module],
+                                'package_metadata',
+                                {})
+        version = self.metadata.get('Version')
+        parser_args = {'description': self.metadata.get('Summary')}
+        if version:
+            parser_args['version'] = "%prog " + version
+        self.parser = optparse.OptionParser(**parser_args)
+        self.add_options(self.parser)
+        (self.options, self.args) = self.parser.parse_args(args)
+
+        if getattr(self.options, 'info', None):
+            self.print_metadata()
+            sys.exit(0)
+
+        # choose appropriate runner and profile classes
+        try:
+            self.runner_class = runners[self.options.app]
+        except KeyError:
+            self.parser.error('Application "%s" unknown (should be one of "firefox" or "thunderbird")' % self.options.app)
+
+    def add_options(self, parser):
+        """add options to the parser"""
+
+        # add profile options
+        MozProfileCLI.add_options(self, parser)
+
+        # add runner options
+        parser.add_option('-b', "--binary",
+                          dest="binary", help="Binary path.",
+                          metavar=None, default=None)
+        parser.add_option('--app', dest='app', default='firefox',
+                          help="Application to use [DEFAULT: %default]")
+        parser.add_option('--app-arg', dest='appArgs',
+                          default=[], action='append',
+                          help="provides an argument to the test application")
+        if self.metadata:
+            parser.add_option("--info", dest="info", default=False,
+                              action="store_true",
+                              help="Print module information")
+
+    ### methods for introspecting data
+
+    def get_metadata_from_egg(self):
+        import pkg_resources
+        ret = {}
+        dist = pkg_resources.get_distribution(self.module)
+        if dist.has_metadata("PKG-INFO"):
+            for line in dist.get_metadata_lines("PKG-INFO"):
+                key, value = line.split(':', 1)
+                ret[key] = value
+        if dist.has_metadata("requires.txt"):
+            ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
+        return ret
+
+    def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
+                                   "Author", "Author-email", "License", "Platform", "Dependencies")):
+        for key in data:
+            if key in self.metadata:
+                print key + ": " + self.metadata[key]
+
+    ### methods for running
+
+    def command_args(self):
+        """additional arguments for the mozilla application"""
+        return self.options.appArgs
+
+    def runner_args(self):
+        """arguments to instantiate the runner class"""
+        return dict(cmdargs=self.command_args(),
+                    binary=self.options.binary,
+                    profile_args=self.profile_args())
+
+    def create_runner(self):
+        return self.runner_class.create(**self.runner_args())
+
+    def run(self):
+        runner = self.create_runner()
+        self.start(runner)
+        runner.cleanup()
+
+    def start(self, runner):
+        """Starts the runner and waits for Firefox to exit or Keyboard Interrupt.
+        Shoule be overwritten to provide custom running of the runner instance."""
+        runner.start()
+        print 'Starting:', ' '.join(runner.command)
+        try:
+            runner.wait()
+        except KeyboardInterrupt:
+            runner.stop()
+
+
+def cli(args=sys.argv[1:]):
+    CLI(args).run()
+
+if __name__ == '__main__':
+    cli()
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/mozrunner/utils.py
@@ -0,0 +1,99 @@
+#!/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 mozrunner.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008-2009
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Mikeal Rogers <mikeal.rogers@gmail.com>
+#   Clint Talbert <ctalbert@mozilla.com>
+#   Henrik Skupin <hskupin@mozilla.com>
+#   Jeff Hammel <jhammel@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 *****
+
+"""
+utility functions for mozrunner
+"""
+
+__all__ = ['findInPath', 'get_metadata_from_egg']
+
+import mozinfo
+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)
+        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
+                        continue
+                    else:
+                        key = key.strip()
+                        value = value.strip()
+                        ret[key] = value
+
+                key, value = line.split(':', 1)
+                key = key.strip()
+                value = value.strip()
+                ret[key] = value
+        if dist.has_metadata("requires.txt"):
+            ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
+        return ret
+except ImportError:
+    # package resources not avaialable
+    def get_metadata_from_egg(module):
+        return {}
+
+
+def findInPath(fileName, path=os.environ['PATH']):
+    """python equivalent of which; should really be in the stdlib"""
+    dirs = path.split(os.pathsep)
+    for dir in dirs:
+        if os.path.isfile(os.path.join(dir, fileName)):
+            return os.path.join(dir, fileName)
+        if mozinfo.isWin:
+            if os.path.isfile(os.path.join(dir, fileName + ".exe")):
+                return os.path.join(dir, fileName + ".exe")
+
+if __name__ == '__main__':
+    for i in sys.argv[1:]:
+        print findInPath(i)
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/mozrunner/setup.py
@@ -0,0 +1,84 @@
+# ***** 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 mozrunner.
+#
+# The Initial Developer of the Original Code is
+#   The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Mikeal Rogers <mikeal.rogers@gmail.com>
+#   Clint Talbert <ctalbert@mozilla.com>
+#   Jeff Hammel <jhammel@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 *****
+
+import os
+import sys
+from setuptools import setup, find_packages
+
+PACKAGE_NAME = "mozrunner"
+PACKAGE_VERSION = "4.0"
+
+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']
+
+# 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,
+      author='Mikeal Rogers, Mozilla',
+      author_email='mikeal.rogers@gmail.com',
+      url='http://github.com/mozautomation/mozmill',
+      license='MPL 1.1/GPL 2.0/LGPL 2.1',
+      packages=find_packages(exclude=['legacy']),
+      zip_safe=False,
+      entry_points="""
+          [console_scripts]
+          mozrunner = mozrunner:cli
+        """,
+      platforms =['Any'],
+      install_requires = deps,
+      classifiers=['Development Status :: 4 - Beta',
+                   'Environment :: Console',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
+                   'Operating System :: OS Independent',
+                   'Topic :: Software Development :: Libraries :: Python Modules',
+                  ]
+     )
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/setup_development.py
@@ -0,0 +1,235 @@
+#!/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 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):
+#   Jeff Hammel <jhammel@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 *****
+
+"""
+Setup mozbase packages for development.
+
+Packages may be specified as command line arguments.
+If no arguments are given, install all packages.
+
+See https://wiki.mozilla.org/Auto-tools/Projects/MozBase
+"""
+
+# XXX note that currently directory names must equal package names
+
+import pkg_resources
+import os
+import sys
+from optparse import OptionParser
+
+from subprocess import PIPE
+try:
+    from subprocess import check_call as call
+except ImportError:
+    from subprocess import call
+
+
+# directory containing this file
+here = os.path.dirname(os.path.abspath(__file__))
+
+# all python packages
+all_packages = [i for i in os.listdir(here)
+                if os.path.exists(os.path.join(here, i, 'setup.py'))]
+
+def cycle_check(order, dependencies):
+    """ensure no cyclic dependencies"""
+    order_dict = dict([(j, i) for i, j in enumerate(order)])
+    for package, deps in dependencies.items():
+        index = order_dict[package]
+        for d in deps:
+            assert index > order_dict[d], "Cyclic dependencies detected"
+
+def dependencies(directory):
+    """
+    get the dependencies of a package directory containing a setup.py
+    returns the package name and the list of dependencies
+    """
+    assert os.path.exists(os.path.join(directory, 'setup.py'))
+
+    # setup the egg info
+    call([sys.executable, 'setup.py', 'egg_info'], cwd=directory, stdout=PIPE)
+
+    # get the .egg-info directory
+    egg_info = [i for i in os.listdir(directory)
+                if i.endswith('.egg-info')]
+    assert len(egg_info) == 1, 'Expected one .egg-info directory in %s, got: %s' % (directory, egg_info)
+    egg_info = os.path.join(directory, egg_info[0])
+    assert os.path.isdir(egg_info), "%s is not a directory" % egg_info
+
+    # read the dependencies
+    requires = os.path.join(egg_info, 'requires.txt')
+    if os.path.exists(requires):
+        dependencies = [i.strip() for i in file(requires).readlines() if i.strip()]
+    else:
+        dependencies = []
+
+    # read the package information
+    pkg_info = os.path.join(egg_info, 'PKG-INFO')
+    info_dict = {}
+    for line in file(pkg_info).readlines():
+        if not line or line[0].isspace():
+            continue # XXX neglects description
+        assert ':' in line
+        key, value = [i.strip() for i in line.split(':', 1)]
+        info_dict[key] = value
+
+
+    # return the information
+    return info_dict['Name'], dependencies
+
+def sanitize_dependency(dep):
+    """
+    remove version numbers from deps
+    """
+    for joiner in ('==', '<=', '>='):
+        if joiner in dep:
+            dep = dep.split(joiner, 1)[0].strip()
+            return dep # XXX only one joiner allowed right now
+    return dep
+
+
+def unroll_dependencies(dependencies):
+    """
+    unroll a set of dependencies to a flat list
+
+    dependencies = {'packageA': set(['packageB', 'packageC', 'packageF']),
+                    'packageB': set(['packageC', 'packageD', 'packageE', 'packageG']),
+                    'packageC': set(['packageE']),
+                    'packageE': set(['packageF', 'packageG']),
+                    'packageF': set(['packageG']),
+                    'packageX': set(['packageA', 'packageG'])}
+    """
+
+    order = []
+
+    # flatten all
+    packages = set(dependencies.keys())
+    for deps in dependencies.values():
+        packages.update(deps)
+
+    while len(order) != len(packages):
+
+        for package in packages.difference(order):
+            if set(dependencies.get(package, set())).issubset(order):
+                order.append(package)
+                break
+        else:
+            raise AssertionError("Cyclic dependencies detected")
+
+    cycle_check(order, dependencies) # sanity check
+
+    return order
+
+
+def main(args=sys.argv[1:]):
+
+    # parse command line options
+    usage = '%prog [options] [package] [package] [...]'
+    parser = OptionParser(usage=usage, description=__doc__)
+    parser.add_option('-d', '--dependencies', dest='list_dependencies',
+                      action='store_true', default=False,
+                      help="list dependencies for the packages")
+    parser.add_option('--list', action='store_true', default=False,
+                      help="list what will be installed")
+    options, packages = parser.parse_args(args)
+
+    if not packages:
+        # install all packages
+        packages = sorted(all_packages)
+
+    # ensure specified packages are in the list
+    assert set(packages).issubset(all_packages), "Packages should be in %s (You gave: %s)" % (all_packages, packages)
+
+    if options.list_dependencies:
+        # list the package dependencies
+        for package in packages:
+            print '%s: %s' % dependencies(os.path.join(here, package))
+        parser.exit()
+
+    # gather dependencies
+    deps = {}
+    mapping = {} # mapping from subdir name to package name
+    # core dependencies
+    for package in packages:
+        key, value = dependencies(os.path.join(here, package))
+        deps[key] = [sanitize_dependency(dep) for dep in value]
+        mapping[package] = key
+    # indirect dependencies
+    flag = True
+    while flag:
+        flag = False
+        for value in deps.values():
+            for dep in value:
+                if dep in all_packages and dep not in deps:
+                    key, value = dependencies(os.path.join(here, dep))
+                    deps[key] = [sanitize_dependency(dep) for dep in value]
+                    mapping[package] = key
+                    flag = True
+                    break
+            if flag:
+                break
+
+    # get the remaining names for the mapping
+    for package in all_packages:
+        if package in mapping:
+            continue
+        key, value = dependencies(os.path.join(here, package))
+        mapping[package] = key
+
+    # unroll dependencies
+    unrolled = unroll_dependencies(deps)
+
+    # make a reverse mapping: package name -> subdirectory
+    reverse_mapping = dict([(j,i) for i, j in mapping.items()])
+
+    # we only care about dependencies in mozbase
+    unrolled = [package for package in unrolled if package in reverse_mapping]
+
+    if options.list:
+        # list what will be installed
+        for package in unrolled:
+            print package
+        parser.exit()
+
+    # set up the packages for development
+    for package in unrolled:
+        call([sys.executable, 'setup.py', 'develop'],
+             cwd=os.path.join(here, reverse_mapping[package]))
+
+if __name__ == '__main__':
+    main()
--- a/testing/testsuite-targets.mk
+++ b/testing/testsuite-targets.mk
@@ -228,17 +228,17 @@ xpcshell-tests-remote:
           echo "please prepare your host with environment variables for TEST_DEVICE"; \
         fi
 
 # 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
+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
 PKG_STAGE = $(DIST)/universal/test-package-stage
 endif
 
 package-tests:
 	@rm -f "$(DIST)/$(PKG_PATH)$(TEST_PACKAGE)"
 ifndef UNIVERSAL_BINARY
@@ -250,17 +250,17 @@ endif
 	cd $(PKG_STAGE) && \
 	  zip -r9D "$(call core_abspath,$(DIST)/$(PKG_PATH)$(TEST_PACKAGE))" *
 
 ifeq (Android, $(OS_TARGET))
 package-tests: stage-android
 endif
 
 make-stage-dir:
-	rm -rf $(PKG_STAGE) && $(NSINSTALL) -D $(PKG_STAGE) && $(NSINSTALL) -D $(PKG_STAGE)/bin && $(NSINSTALL) -D $(PKG_STAGE)/bin/components && $(NSINSTALL) -D $(PKG_STAGE)/certs && $(NSINSTALL) -D $(PKG_STAGE)/jetpack && $(NSINSTALL) -D $(PKG_STAGE)/firebug && $(NSINSTALL) -D $(PKG_STAGE)/peptest
+	rm -rf $(PKG_STAGE) && $(NSINSTALL) -D $(PKG_STAGE) && $(NSINSTALL) -D $(PKG_STAGE)/bin && $(NSINSTALL) -D $(PKG_STAGE)/bin/components && $(NSINSTALL) -D $(PKG_STAGE)/certs && $(NSINSTALL) -D $(PKG_STAGE)/jetpack && $(NSINSTALL) -D $(PKG_STAGE)/firebug && $(NSINSTALL) -D $(PKG_STAGE)/peptest && $(NSINSTALL) -D $(PKG_STAGE)/mozbase
 
 stage-mochitest: make-stage-dir
 	$(MAKE) -C $(DEPTH)/testing/mochitest stage-package
 
 stage-reftest: make-stage-dir
 	$(MAKE) -C $(DEPTH)/layout/tools/reftest stage-package
 
 stage-xpcshell: make-stage-dir
@@ -278,14 +278,17 @@ stage-android: make-stage-dir
 stage-jetpack: make-stage-dir
 	$(NSINSTALL) $(topsrcdir)/testing/jetpack/jetpack-location.txt $(PKG_STAGE)/jetpack
 
 stage-firebug: make-stage-dir
 	$(MAKE) -C $(DEPTH)/testing/firebug stage-package
 
 stage-peptest: make-stage-dir
 	$(MAKE) -C $(DEPTH)/testing/peptest stage-package
+
+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 \
-  package-tests make-stage-dir stage-mochitest stage-reftest stage-xpcshell stage-jstests stage-android stage-jetpack stage-firebug stage-peptest
+  package-tests make-stage-dir stage-mochitest stage-reftest stage-xpcshell stage-jstests stage-android stage-jetpack stage-firebug stage-peptest stage-mozbase
--- a/toolkit/toolkit-makefiles.sh
+++ b/toolkit/toolkit-makefiles.sh
@@ -886,16 +886,17 @@ if [ "$ENABLE_TESTS" ]; then
     testing/mochitest/tests/MochiKit-1.4.2/tests/SimpleTest/Makefile
     testing/mochitest/tests/SimpleTest/Makefile
     testing/mochitest/tests/browser/Makefile
     testing/tools/screenshot/Makefile
     testing/xpcshell/Makefile
     testing/xpcshell/example/Makefile
     testing/firebug/Makefile
     testing/peptest/Makefile
+    testing/mozbase/Makefile
     toolkit/components/alerts/test/Makefile
     toolkit/components/autocomplete/tests/Makefile
     toolkit/components/commandlines/test/Makefile
     toolkit/components/contentprefs/tests/Makefile
     toolkit/components/downloads/test/Makefile
     toolkit/components/downloads/test/browser/Makefile
     toolkit/components/microformats/tests/Makefile
     toolkit/components/passwordmgr/test/browser/Makefile
--- a/toolkit/toolkit-tiers.mk
+++ b/toolkit/toolkit-tiers.mk
@@ -263,10 +263,11 @@ ifdef MOZ_MAPINFO
 tier_platform_dirs	+= tools/codesighs
 endif
 
 ifdef ENABLE_TESTS
 tier_platform_dirs += testing/mochitest
 tier_platform_dirs += testing/xpcshell
 tier_platform_dirs += testing/tools/screenshot
 tier_platform_dirs += testing/peptest
+tier_platform_dirs += testing/mozbase
 endif