Bug 631659 - Push Firebug Test Runner to m-c. Patch 2.3. r=ctalbert, a=NPOTB
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Sat, 26 Feb 2011 13:19:57 -0500
changeset 63140 aca8013c8cff17c12a7859b6d00855d2b6648f34
parent 63139 a95e6e05435177ce307cd5c1e264b221ab8cd9b9
child 63141 1df2e6e48609777f6cd3ae4654706ec728a02ed4
push id19040
push userjmaher@mozilla.com
push dateSat, 26 Feb 2011 18:20:43 +0000
treeherdermozilla-central@1df2e6e48609 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersctalbert, NPOTB
bugs631659
milestone2.0b13pre
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 631659 - Push Firebug Test Runner to m-c. Patch 2.3. r=ctalbert, a=NPOTB
testing/firebug/Makefile.in
testing/firebug/fb-test-runner.config
testing/firebug/fb_run.py
testing/firebug/installfirebug.py
testing/firebug/mozprocess/mozprocess/__init__.py
testing/firebug/mozprocess/mozprocess/killableprocess.py
testing/firebug/mozprocess/mozprocess/pid.py
testing/firebug/mozprocess/mozprocess/qijo.py
testing/firebug/mozprocess/mozprocess/winprocess.py
testing/firebug/mozprocess/mozprocess/wpk.py
testing/firebug/mozprocess/setup.py
testing/firebug/mozprofile/mozprofile/__init__.py
testing/firebug/mozprofile/mozprofile/profile.py
testing/firebug/mozprofile/setup.py
testing/firebug/mozrunner/mozrunner/__init__.py
testing/firebug/mozrunner/mozrunner/runner.py
testing/firebug/mozrunner/mozrunner/utils.py
testing/firebug/mozrunner/setup.py
testing/testsuite-targets.mk
toolkit/toolkit-makefiles.sh
new file mode 100644
--- /dev/null
+++ b/testing/firebug/Makefile.in
@@ -0,0 +1,86 @@
+# ***** 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):
+# Andrew Halberstadt <ahalberstadt@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 *****
+
+DEPTH = ../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+mozmilldir = @top_srcdir@/testing/mozmill
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = testing_firebug
+
+include $(topsrcdir)/config/rules.mk
+
+# Firebug requires several of the same packages as Mozmill.
+# Rather than check them into m-c twice, just grab them from
+# the Mozmill directory.
+TEST_MOZMILL_PACKAGES = \
+  simplejson-2.1.1 \
+  $(NULL)
+
+TEST_MOZMILL_EXTRAS = \
+  virtualenv \
+  $(NULL)
+
+# 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.
+TEST_HARNESS_PACKAGES = \
+  mozprofile \
+  mozprocess \
+  mozrunner \
+  $(NULL)
+
+TEST_HARNESS_EXTRAS = \
+  installfirebug.py \
+  $(NULL)
+  
+TEST_HARNESS_FILES = \
+  fb_run.py \
+  $(NULL)
+
+stage-package: PKG_STAGE = $(DIST)/test-package-stage
+stage-package:
+	$(NSINSTALL) -D $(PKG_STAGE)/firebug
+	@echo $(TEST_MOZMILL_PACKAGES) $(TEST_HARNESS_PACKAGES) > $(PKG_STAGE)/firebug/PACKAGES
+	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_PACKAGES)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
+	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_FILES)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
+	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_EXTRAS)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
+	@(cd $(mozmilldir) && tar $(TAR_CREATE_FLAGS) - $(TEST_MOZMILL_EXTRAS)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
+	@(cd $(mozmilldir) && tar $(TAR_CREATE_FLAGS) - $(TEST_MOZMILL_PACKAGES)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
new file mode 100644
--- /dev/null
+++ b/testing/firebug/fb-test-runner.config
@@ -0,0 +1,18 @@
+# This section contains arguments to be read by the firebug test runner
+[runner_args]
+server = http://10.250.5.0
+
+# This section maps firefox versions to firebug versions.
+# The default value is used for any firefox version not specified here.
+[version_map]
+3.6 = 1.6
+4.0 = 1.7
+4.0b = 1.7
+default = 1.7
+
+# This section allows disabling tests.
+# Use 'test_name' = 'comma separated list of Firefox versions to disable the test against'
+# See the Firebug test console for a full list of tests.
+[disable_tests]
+# Example
+# firebug/testName.js = 3.6,4.0b
new file mode 100644
--- /dev/null
+++ b/testing/firebug/fb_run.py
@@ -0,0 +1,303 @@
+# ***** 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 the Firebug Test Runner.
+#
+# The Initial Developer of the Original Code is
+# Andrew Halberstadt.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Andrew Halberstadt - ahalberstadt@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 mozrunner import FirefoxRunner
+from mozprofile import FirefoxProfile
+from optparse import OptionParser
+from ConfigParser import ConfigParser, NoOptionError, NoSectionError
+from time import sleep
+import logging
+import urllib2
+import os, sys, platform
+
+class FBRunner:
+    def __init__(self, **kwargs):    
+        # Set up the log file or use stdout if none specified
+        logLevel = logging.DEBUG if kwargs["debug"] else logging.INFO
+        filename = kwargs["log"]
+        self.log = logging.getLogger("FIREBUG")
+        if filename:
+            dirname = os.path.dirname(filename)
+            if dirname and not os.path.exists(dirname):
+                os.makedirs(dirname)
+            handler = logging.FileHandler(filename)
+            format = "%(asctime)s - %(name)s %(levelname)s | %(message)s"
+        else:
+            handler = logging.StreamHandler()
+            format = "%(name)s %(levelname)s | %(message)s"
+        handler.setFormatter(logging.Formatter(format))
+        self.log.addHandler(handler)
+        self.log.setLevel(logLevel)
+            
+        # Initialization  
+        self.binary = kwargs["binary"]
+        self.profile = kwargs["profile"]
+        self.serverpath = kwargs["serverpath"]
+        self.version = kwargs["version"]
+        self.testlist = kwargs["testlist"]
+        self.platform = platform.system().lower()
+        
+        # Because the only communication between this script and the FBTest console is the
+        # log file, we don't know whether there was a crash or the test is just taking awhile.
+        # Make 1 minute the timeout for tests.
+        self.TEST_TIMEOUT = 60        
+        
+        # Get version of Firefox being run (only possible if we were passed in a binary)
+        self.appVersion = ""
+        if self.binary:
+            app = ConfigParser()
+            app.read(os.path.join(os.path.dirname(self.binary), "application.ini"))
+            ver = app.get("App", "Version").rstrip("0123456789pre")    # Version should be of the form '3.6' or '4.0b' and not the whole string
+            self.appVersion = ver[:-1] if ver[-1]=="." else ver
+
+        # Read in fb-test-runner.config for local configuration
+        localConfig = ConfigParser()
+        localConfig.read("fb-test-runner.config")
+        if not self.serverpath:
+            self.serverpath = localConfig.get("runner_args", "server")
+        
+        # Ensure serverpath has correct format
+        self.serverpath = self.serverpath.rstrip("/") + "/"
+
+        # Make sure we have a firebug version
+        if not self.version:
+            try:
+                self.version = localConfig.get("version_map", self.appVersion)
+            except NoOptionError:
+                self.version = localConfig.get("version_map", "default")
+                self.log.warning("Could not find an appropriate version of Firebug to use, using Firebug " + self.version)
+
+        # Read in the Firebug team's config file
+        try:
+            self.download(self.serverpath + "releases/firebug/test-bot.config", "test-bot.config")
+        except urllib2.URLError:
+            self.log.error("Could not download test-bot.config, check that '" + self.serverpath + "releases/firebug/test-bot.config' is valid")
+            raise
+        self.config = ConfigParser()
+        self.config.read("test-bot.config")
+        
+        # Make sure we have a testlist
+        if not self.testlist:
+            self.testlist = self.config.get("Firebug"+self.version, "TEST_LIST")
+
+    def cleanup(self):
+        """
+        Remove temporarily downloaded files
+        """
+        try:
+            for tmpFile in ["firebug.xpi", "fbtest.xpi", "test-bot.config"]:
+                if os.path.exists(tmpFile):
+                    self.log.debug("Removing " + tmpFile)
+                    os.remove(tmpFile)
+        except Exception, e:
+            self.log.warn("Could not clean up temporary files: " + str(e))
+            
+    def download(self, url, savepath):
+        """
+        Save the file located at 'url' into 'filename'
+        """
+        self.log.debug("Downloading '" + url + "' to '" + savepath + "'")
+        ret = urllib2.urlopen(url)
+        savedir = os.path.dirname(savepath)
+        if savedir and not os.path.exists(savedir):
+            os.makedirs(savedir)
+        outfile = open(savepath, 'wb')
+        outfile.write(ret.read())
+        outfile.close()
+        
+    def get_extensions(self):
+        """
+        Downloads the firebug and fbtest extensions
+        for the specified Firebug version
+        """
+        self.log.debug("Downloading firebug and fbtest extensions from server")
+        FIREBUG_XPI = self.config.get("Firebug" + self.version, "FIREBUG_XPI")
+        FBTEST_XPI = self.config.get("Firebug" + self.version, "FBTEST_XPI")
+        self.download(FIREBUG_XPI, "firebug.xpi")
+        self.download(FBTEST_XPI, "fbtest.xpi")
+
+    def disable_compatibilityCheck(self):
+        """
+        Disables compatibility check which could
+        potentially prompt the user for action
+        """
+        self.log.debug("Disabling compatibility check")
+        try:
+            prefs = open(os.path.join(self.profile, "prefs.js"), "a")
+            prefs.write("user_pref(\"extensions.checkCompatibility." + self.appVersion + "\", false);\n")
+            prefs.close()
+        except Exception, e:
+            self.log.warn("Could not disable compatibility check: " + str(e))
+
+    def run(self):
+        """
+        Code for running the tests
+        """
+        if self.profile:
+            # Ensure the profile actually exists
+            if not os.path.exists(self.profile):
+                self.log.warn("Profile '" + self.profile + "' doesn't exist.  Creating temporary profile")
+                self.profile = None
+            else:
+                # Move any potential existing log files to log_old folder
+                if os.path.exists(os.path.join(self.profile, "firebug/fbtest/logs")):
+                    self.log.debug("Moving existing log files to archive")
+                    for name in os.listdir(os.path.join(self.profile, "firebug/fbtest/logs")):
+                        os.rename(os.path.join(self.profile, "firebug/fbtest/logs", name), os.path.join(self.profile, "firebug/fbtest/logs_old", name))
+
+        # Grab the extensions from server   
+        try:
+            self.get_extensions()
+        except (NoSectionError, NoOptionError), e:            
+            self.log.error("Extensions could not be downloaded, malformed test-bot.config: " + str(e))
+            self.cleanup()
+            raise
+        except urllib2.URLError, e:
+            self.log.error("Extensions could not be downloaded, urllib2 error: " + str(e))
+            self.cleanup()
+            raise
+    
+        # Create environment variables
+        mozEnv = os.environ
+        mozEnv["XPC_DEBUG_WARN"] = "warn"                # Suppresses certain alert warnings that may sometimes appear
+        mozEnv["MOZ_CRASHREPORTER_NO_REPORT"] = "true"   # Disable crash reporter UI
+
+        # Create profile for mozrunner and start the Firebug tests
+        self.log.info("Starting Firebug Tests")
+        try:
+            self.log.debug("Creating Firefox profile and installing extensions")
+            mozProfile = FirefoxProfile(profile=self.profile, addons=["firebug.xpi", "fbtest.xpi"])
+            self.profile = mozProfile.profile
+            
+            # Disable the compatibility check on startup
+            if self.binary:
+                self.disable_compatibilityCheck()
+            else:
+                self.log.warn("Can't disable compatibility check because binary wasn't specified")
+
+            self.log.debug("Running Firefox with cmdargs '-runFBTests " + self.testlist + "'")
+            mozRunner = FirefoxRunner(profile=mozProfile, binary=self.binary, cmdargs=["-runFBTests", self.testlist], env=mozEnv)         
+            mozRunner.start()
+        except Exception, e:
+            self.log.error("Could not start Firefox: " + str(e))
+            self.cleanup()
+            raise
+
+        # Find the log file
+        timeout, logfile = 0, 0
+        # Wait up to 60 seconds for the log file to be initialized
+        while not logfile and timeout < 60:
+            try:
+                for name in os.listdir(os.path.join(self.profile, "firebug/fbtest/logs")):
+                    logfile = open(os.path.join(self.profile, "firebug/fbtest/logs/", name))
+            except OSError:
+                timeout += 1
+                sleep(1)
+                
+        # If log file was not found
+        if not logfile:
+            self.log.error("Could not find the log file in profile '" + self.profile + "'")
+            self.cleanup()
+            raise
+        # If log file found, exit when fbtests finished (if no activity, wait up self.TEST_TIMEOUT)
+        else:
+            line, timeout = "", 0
+            while timeout < self.TEST_TIMEOUT:
+                line = logfile.readline()
+                if line == "":
+                    sleep(1)
+                    timeout += 1
+                else:
+                    print line.rstrip()
+                    if line.find("Test Suite Finished") != -1:
+                        break
+                    timeout = 0
+        
+        # If there was a timeout, then there was most likely a crash (however could also be failure in FBTest console or test itself)
+        if timeout >= self.TEST_TIMEOUT:
+            logfile.seek(1)
+            line = logfile.readlines()[-1]
+            if line.find("FIREBUG INFO") != -1:
+                line = line[line.find("|") + 1:].lstrip()   # Extract the test name from log line
+                line = line[:line.find("|")].rstrip()
+            else:
+                line = "Unknown Test"
+            print "FIREBUG TEST-UNEXPECTED-FAIL | " + line + " | Possible Firefox crash detected"       # Print out crash message with offending test
+            self.log.warn("Possible crash detected - test run aborted")
+            
+        # Cleanup
+        logfile.close()
+        mozRunner.stop()
+        self.cleanup()
+        self.log.debug("Exiting - Status successful")
+
+
+# Called from the command line
+def cli(argv=sys.argv[1:]):
+    parser = OptionParser("usage: %prog [options]")
+    parser.add_option("--appname", dest="binary",
+                      help="Firefox binary path")
+                    
+    parser.add_option("--profile-path", dest="profile",
+                      help="The profile to use when running Firefox")
+                        
+    parser.add_option("-s", "--serverpath", dest="serverpath", 
+                      help="The http server containing the Firebug tests")
+                        
+    parser.add_option("-v", "--version", dest="version",
+                      help="The firebug version to run")
+                        
+    parser.add_option("-t", "--testlist", dest="testlist",
+                      help="Specify the name of the testlist to use, should usually use the default")
+                      
+    parser.add_option("--log", dest="log",
+                      help="Path to the log file (default is stdout)")
+                      
+    parser.add_option("--debug", dest="debug",
+                      action="store_true",
+                      help="Enable debug logging")
+    (opt, remainder) = parser.parse_args(argv)
+    
+    try:
+        runner = FBRunner(binary=opt.binary, profile=opt.profile, serverpath=opt.serverpath, 
+                                    version=opt.version, testlist=opt.testlist, log=opt.log, debug=opt.debug)
+        runner.run()
+    except Exception:
+        return -1
+
+if __name__ == '__main__':
+	sys.exit(cli())
new file mode 100644
--- /dev/null
+++ b/testing/firebug/installfirebug.py
@@ -0,0 +1,147 @@
+#!/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):
+#     Andrew Halberstadt <ahalberstadt@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 *****
+
+"""
+install firebug and its dependencies
+"""
+
+import os
+import sys
+from optparse import OptionParser
+from subprocess import call
+
+### utility functions for cross-platform
+
+def is_windows():
+    return sys.platform.startswith('win')
+
+def esc(path):
+    """quote and escape a path for cross-platform use"""
+    return '"%s"' % repr(path)[1:-1]
+
+def scripts_path(virtual_env):
+    """path to scripts directory"""
+    if is_windows():
+        return os.path.join(virtual_env, 'Scripts')
+    return os.path.join(virtual_env, 'bin')
+
+def python_script_path(virtual_env, script_name):
+    """path to a python script in a virtualenv"""
+    scripts_dir = scripts_path(virtual_env)
+    if is_windows():
+        script_name = script_name + '-script.py'
+    return os.path.join(scripts_dir, script_name)
+
+def entry_point_path(virtual_env, entry_point):
+    path = os.path.join(scripts_path(virtual_env), entry_point)
+    if is_windows():
+        path += '.exe'
+    return path
+
+### command-line entry point
+
+def main(args=None):
+    """command line front-end function"""
+
+    # parse command line arguments
+    args = args or sys.argv[1:]
+    usage = "Usage: %prog [options] [destination]"
+    parser = OptionParser(usage=usage)
+    parser.add_option('--develop', dest='develop',
+                      action='store_true', default=False,
+                      help='setup in development mode')
+    options, args = parser.parse_args(args)
+
+    # Print the python version
+    print 'Python: %s' % sys.version
+
+    # The data is kept in the same directory as the script
+    source=os.path.abspath(os.path.dirname(__file__))
+
+    # directory to install to
+    if not len(args):
+        destination = source
+    elif len(args) == 1:
+        destination = os.path.abspath(args[0])
+    else:
+        parser.print_usage()
+        parser.exit(1)
+
+    os.chdir(source)
+
+    # check for existence of necessary files
+    if not os.path.exists('virtualenv'):
+        print "File not found: virtualenv"
+        sys.exit(1)
+    PACKAGES_FILE = 'PACKAGES'
+    if not os.path.exists(PACKAGES_FILE) and destination != source:
+        PACKAGES_FILE = os.path.join(destination, PACKAGES_FILE)
+    if not os.path.exists(PACKAGES_FILE):
+        print "File not found: PACKAGES"
+
+    # packages to install in dependency order
+    PACKAGES=file(PACKAGES_FILE).read().split()
+    assert PACKAGES
+  
+    # create the virtualenv and install packages
+    env = os.environ.copy()
+    env.pop('PYTHONHOME', None)
+    returncode = call([sys.executable, os.path.join('virtualenv', 'virtualenv.py'), destination], env=env)
+    if returncode:
+        print 'Failure to install virtualenv'
+        sys.exit(returncode)
+    if options.develop:
+        python = entry_point_path(destination, 'python')
+        for package in PACKAGES:
+            oldcwd = os.getcwd()
+            os.chdir(package)
+            returncode = call([python, 'setup.py', 'develop'])
+            os.chdir(oldcwd)
+            if returncode:
+                break
+    else:
+        pip = entry_point_path(destination, 'pip')
+        returncode = call([pip, 'install'] + PACKAGES, env=env)
+
+    if returncode:
+        print 'Failure to install packages'
+        sys.exit(returncode)
+
+if __name__ == '__main__':
+    main()
new file mode 100644
--- /dev/null
+++ b/testing/firebug/mozprocess/mozprocess/__init__.py
@@ -0,0 +1,1 @@
+#
new file mode 100644
--- /dev/null
+++ b/testing/firebug/mozprocess/mozprocess/killableprocess.py
@@ -0,0 +1,292 @@
+# killableprocess - subprocesses which can be reliably killed
+#
+# Parts of this module are copied from the subprocess.py file contained
+# in the Python distribution.
+#
+# 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.
+
+"""killableprocess - Subprocesses which can be reliably killed
+
+This module is a subclass of the builtin "subprocess" module. It allows
+processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method.
+
+It also adds a timeout argument to Wait() for a limited period of time before
+forcefully killing the process.
+
+Note: On Windows, this module requires Windows 2000 or higher (no support for
+Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with
+Python 2.5+ or available from http://python.net/crew/theller/ctypes/
+"""
+
+import subprocess
+import sys
+import os
+import time
+import datetime
+import types
+import exceptions
+
+try:
+    from subprocess import CalledProcessError
+except ImportError:
+    # Python 2.4 doesn't implement CalledProcessError
+    class CalledProcessError(Exception):
+        """This exception is raised when a process run by check_call() returns
+        a non-zero exit status. The exit status will be stored in the
+        returncode attribute."""
+        def __init__(self, returncode, cmd):
+            self.returncode = returncode
+            self.cmd = cmd
+        def __str__(self):
+            return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
+
+mswindows = (sys.platform == "win32")
+
+if mswindows:
+    import winprocess
+else:
+    import signal
+
+def call(*args, **kwargs):
+    waitargs = {}
+    if "timeout" in kwargs:
+        waitargs["timeout"] = kwargs.pop("timeout")
+
+    return Popen(*args, **kwargs).wait(**waitargs)
+
+def check_call(*args, **kwargs):
+    """Call a program with an optional timeout. If the program has a non-zero
+    exit status, raises a CalledProcessError."""
+
+    retcode = call(*args, **kwargs)
+    if retcode:
+        cmd = kwargs.get("args")
+        if cmd is None:
+            cmd = args[0]
+        raise CalledProcessError(retcode, cmd)
+
+if not mswindows:
+    def DoNothing(*args):
+        pass
+
+class Popen(subprocess.Popen):
+    kill_called = False
+    if mswindows:
+        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, types.StringTypes):
+                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()
+
+            # set process creation flags
+            creationflags |= winprocess.CREATE_SUSPENDED
+            creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT
+            if canCreateJob:
+                creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB
+
+            # 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:
+                # We create a new job for this process, so that we can kill
+                # the process and any sub-processes 
+                self._job = winprocess.CreateJobObject()
+                winprocess.AssignProcessToJobObject(self._job, int(hp))
+            else:
+                self._job = None
+                    
+            winprocess.ResumeThread(int(ht))
+            ht.Close()
+
+            if p2cread is not None:
+                p2cread.Close()
+            if c2pwrite is not None:
+                c2pwrite.Close()
+            if errwrite is not None:
+                errwrite.Close()
+            time.sleep(.1)
+
+    def kill(self, group=True):
+        """Kill the process. If group=True, all sub-processes will also be killed."""
+        self.kill_called = True
+        if mswindows:
+            if group and self._job:
+                winprocess.TerminateJobObject(self._job, 127)
+            else:
+                try:
+                    winprocess.TerminateProcess(self._handle, 127)
+                except:
+                    # TODO: better error handling here
+                    pass
+            self.returncode = 127    
+        else:
+            if group:
+                try:
+                    os.killpg(self.pid, signal.SIGKILL)
+                except: pass
+            else:
+                os.kill(self.pid, signal.SIGKILL)
+            self.returncode = -9
+
+    def wait(self, timeout=None, group=True):
+        """Wait for the process to terminate. Returns returncode attribute.
+        If timeout seconds are reached and the process has not terminated,
+        it will be forcefully killed. If timeout is -1, wait will not
+        time out."""
+        
+        if timeout is not None:
+            # timeout is now in milliseconds
+            timeout = timeout * 1000
+
+        if self.returncode is not None:
+            return self.returncode
+
+        starttime = datetime.datetime.now()
+
+        if mswindows:
+            if timeout is None:
+                timeout = -1
+            rc = winprocess.WaitForSingleObject(self._handle, timeout)
+            
+            if rc != winprocess.WAIT_TIMEOUT:
+                def check():
+                    now = datetime.datetime.now()
+                    diff = now - starttime
+                    if (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000):
+                        if self._job:
+                            if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0):
+                                return True
+                        else:
+                            return True
+                    return False
+                while check():
+                    time.sleep(.5)
+            
+            now = datetime.datetime.now()
+            diff = now - starttime
+            if (diff.seconds * 1000 * 1000 + diff.microseconds) > (timeout * 1000):
+                self.kill(group)
+            else:
+                self.returncode = winprocess.GetExitCodeProcess(self._handle)
+        else:
+            if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
+                def group_wait(timeout):
+                    try:
+                        os.waitpid(self.pid, 0)
+                    except OSError, e:
+                        pass # If wait has already been called on this pid, bad things happen
+                    return self.returncode
+            elif sys.platform == 'darwin':
+                def group_wait(timeout):
+                    try:
+                        count = 0
+                        if timeout is None and self.kill_called:
+                            timeout = 10 # Have to set some kind of timeout or else this could go on forever
+                        if timeout is None:
+                            while 1:
+                                os.killpg(self.pid, signal.SIG_DFL)
+                        while ((count * 2) <= timeout):
+                            os.killpg(self.pid, signal.SIG_DFL)
+                            # count is increased by 500ms for every 0.5s of sleep
+                            time.sleep(.5); count += 500
+                    except exceptions.OSError:
+                        return self.returncode
+                        
+            if timeout is None:
+                if group is True:
+                    return group_wait(timeout)
+                else:
+                    subprocess.Popen.wait(self)
+                    return self.returncode
+
+            returncode = False
+
+            now = datetime.datetime.now()
+            diff = now - starttime
+            while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and ( returncode is False ):
+                if group is True:
+                    return group_wait(timeout)
+                else:
+                    if subprocess.poll() is not None:
+                        returncode = self.returncode
+                time.sleep(.5)
+                now = datetime.datetime.now()
+                diff = now - starttime
+            return self.returncode
+                
+        return self.returncode
+    # We get random maxint errors from subprocesses __del__
+    __del__ = lambda self: None        
+        
+def setpgid_preexec_fn():
+    os.setpgid(0, 0)
+        
+def runCommand(cmd, **kwargs):
+    if sys.platform != "win32":
+        return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs)
+    else:
+        return Popen(cmd, **kwargs)
new file mode 100644
--- /dev/null
+++ b/testing/firebug/mozprocess/mozprocess/pid.py
@@ -0,0 +1,68 @@
+#!/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 Corporation Code.
+#
+# The Initial Developer of the Original Code is
+# Mikeal Rogers.
+# 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 *****
+
+import os
+import subprocess
+import sys
+
+def get_pids(name, minimun_pid=0):
+  """Get all the pids matching name, exclude any pids below minimum_pid."""
+  # XXX see also https://bugzilla.mozilla.org/show_bug.cgi?id=592750
+  
+  if os.name == 'nt' or sys.platform == 'cygwin':
+    import wpk
+    pids = wpk.get_pids(name)
+  else:
+    process = subprocess.Popen(['ps', 'ax'], stdout=subprocess.PIPE)
+    output, _ = process.communicate()
+    data = output.splitlines()
+    pids = [int(line.split()[0]) for line in data if line.find(name) is not -1]
+
+  matching_pids = [m for m in pids if m > minimun_pid]
+  return matching_pids
+
+if __name__ == '__main__':
+  import sys
+  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/firebug/mozprocess/mozprocess/qijo.py
@@ -0,0 +1,162 @@
+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)
+                ]
+
+# 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)]
+
+# XXX Magical numbers like 8 should be documented
+JobObjectBasicAndIoAccountingInformation = 8
+
+# ...like magical number 9 comes from
+# http://community.flexerasoftware.com/archive/index.php?t-181670.html
+# I wish I had a more canonical source
+JobObjectExtendedLimitInformation = 9
+
+class JobObjectInfo(object):
+    mapping = { 'JobObjectBasicAndIoAccountingInformation': 8,
+                'JobObjectExtendedLimitInformation': 9
+                }
+    structures = { 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)
+
+def test_qijo():
+    from killableprocess import Popen
+
+    popen = Popen('c:\\windows\\notepad.exe')
+
+    try:
+        result = QueryInformationJobObject(0, 8)
+        raise AssertionError('throw should occur')
+    except WindowsError, e:
+        pass
+
+    try:
+        result = QueryInformationJobObject(0, 1)
+        raise AssertionError('throw should occur')
+    except NotImplementedError, e:
+        pass
+
+    result = QueryInformationJobObject(popen._job, 8)
+    if result['BasicInfo']['ActiveProcesses'] != 1:
+        raise AssertionError('expected ActiveProcesses to be 1')
+    popen.kill()
+
+    result = QueryInformationJobObject(popen._job, 8)
+    if result.BasicInfo.ActiveProcesses != 0:
+        raise AssertionError('expected ActiveProcesses to be 0')
+
+if __name__ == '__main__':
+    print "testing."
+    test_qijo()
+    print "success!"
new file mode 100644
--- /dev/null
+++ b/testing/firebug/mozprocess/mozprocess/winprocess.py
@@ -0,0 +1,381 @@
+# 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, windll, WinError, WINFUNCTYPE
+from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD
+from qijo import QueryInformationJobObject
+
+LPVOID = c_void_p
+LPBYTE = POINTER(BYTE)
+LPDWORD = POINTER(DWORD)
+LPBOOL = POINTER(BOOL)
+
+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))
+        
+# 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 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
+
+# XXX these flags should be documented
+DEBUG_ONLY_THIS_PROCESS = 0x00000002
+DEBUG_PROCESS = 0x00000001
+DETACHED_PROCESS = 0x00000008
+
+# 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)
+
+INFINITE = -1
+WAIT_TIMEOUT = 0x0102
+WAIT_OBJECT_0 = 0x0
+WAIT_ABANDONED = 0x0080
+
+# 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()
+
+if __name__ == '__main__':
+    import sys
+    from killableprocess import Popen
+    nargs = len(sys.argv[1:])
+    if nargs:
+        if nargs != 1 or sys.argv[1] != '-child':
+            raise AssertionError('Wrong flags; run like `python /path/to/winprocess.py`')
+        child()
+    else:
+        parent()
new file mode 100644
--- /dev/null
+++ b/testing/firebug/mozprocess/mozprocess/wpk.py
@@ -0,0 +1,76 @@
+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/firebug/mozprocess/setup.py
@@ -0,0 +1,25 @@
+from setuptools import setup, find_packages
+
+version = '0.1a'
+
+setup(name='mozprocess',
+      version=version,
+      description="Mozilla-authored process handling",
+      long_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=[
+          # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )
new file mode 100644
--- /dev/null
+++ b/testing/firebug/mozprofile/mozprofile/__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 Mozilla Corporation Code.
+#
+# The Initial Developer of the Original Code is
+# Mikeal Rogers.
+# 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 profile import *
new file mode 100644
--- /dev/null
+++ b/testing/firebug/mozprofile/mozprofile/profile.py
@@ -0,0 +1,252 @@
+# ***** 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 Corporation Code.
+#
+# The Initial Developer of the Original Code is
+# Mikeal Rogers.
+# 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 *****
+
+__all__ = ['Profile', 'FirefoxProfile', 'ThunderbirdProfile', 'print_addon_ids']
+
+import os
+import sys
+import tempfile
+import zipfile
+from xml.dom import minidom
+
+try:
+    import simplejson
+except ImportError:
+    import json as simplejson
+
+# Use dir_util for copy/rm operations because shutil is all kinds of broken
+from distutils import dir_util
+copytree = dir_util.copy_tree
+rmtree = dir_util.remove_tree
+
+
+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, preferences=None):
+                 
+        # Handle profile creation
+        self.create_new = not profile
+        if profile:
+            self.profile = profile
+        else:
+            self.profile = self.create_new_profile()
+
+        # set preferences from class preferences
+        if hasattr(self.__class__, 'preferences'):
+            self.preferences = self.__class__.preferences.copy()
+        else:
+            self.preferences = {}
+        self.preferences.update(preferences or {})
+        self.set_preferences(self.preferences)
+ 
+        # handle addon installation
+        self.addons_installed = []
+        self.addons = addons or []
+        for addon in self.addons:
+            self.install_addon(addon)
+
+    def reset(self):
+        """
+        reset the profile to the beginning state
+        """
+        self.cleanup()
+        if self.create_new:
+            self.__init__(addons=self.addons, preferences=self.preferences)
+        else:
+            self.__init__(profile=self.profile, addons=self.addons, preferences=self.preferences)
+
+    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 related to addons
+
+    @classmethod
+    def addon_id(self, addon_path):
+        """
+        return the id for a given addon, or None if not found
+        - addon_path : path to the addon directory
+        """
+        
+        def find_id(desc):
+            """finds the addon id give its description"""
+            
+            addon_id = None
+            for elem in desc:
+                apps = elem.getElementsByTagName('em:targetApplication')
+                if apps:
+                    for app in apps:
+                        # remove targetApplication nodes, they contain id's we aren't interested in
+                        elem.removeChild(app)
+                    if elem.getElementsByTagName('em:id'):
+                        addon_id = str(elem.getElementsByTagName('em:id')[0].firstChild.data)
+                    elif elem.hasAttribute('em:id'):
+                        addon_id = str(elem.getAttribute('em:id'))
+            return addon_id
+
+        doc = minidom.parse(os.path.join(addon_path, 'install.rdf')) 
+
+        for tag in 'Description', 'RDF:Description':
+            desc = doc.getElementsByTagName(tag)
+            addon_id = find_id(desc)
+            if addon_id:
+                return addon_id
+
+    def install_addon(self, path):
+        """Installs the given addon or directory of addons in the profile."""
+
+        # 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')):
+            addons = [os.path.join(path, x) for x in os.listdir(path)]
+           
+        for addon in addons:
+            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()
+                addon = tmpdir
+
+            # determine the addon id
+            addon_id = Profile.addon_id(addon)
+            assert addon_id is not None, "The addon id could not be found: %s" % addon
+ 
+            # copy the addon to the profile
+            addon_path = os.path.join(self.profile, 'extensions', addon_id)
+            copytree(addon, addon_path, preserve_symlinks=1)
+            self.addons_installed.append(addon_path)
+
+    def clean_addons(self):
+        """Cleans up addons in the profile."""
+        for addon in self.addons_installed:
+            if os.path.isdir(addon):
+                rmtree(addon)
+
+    ### methods for preferences
+
+    def set_preferences(self, preferences):
+        """Adds preferences dict to profile preferences"""
+        
+        prefs_file = os.path.join(self.profile, 'user.js')
+        
+        # Ensure that the file exists first otherwise create an empty file
+        if os.path.isfile(prefs_file):
+            f = open(prefs_file, 'a+')
+        else:
+            f = open(prefs_file, 'w')
+
+        f.write('\n#MozRunner Prefs Start\n')
+
+        pref_lines = ['user_pref(%s, %s);' %
+                      (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in
+                       preferences.items()]
+        for line in pref_lines:
+            f.write(line+'\n')
+        f.write('#MozRunner Prefs End\n')
+        f.flush() ; f.close()
+
+    def clean_preferences(self):
+        """Removed preferences added by mozrunner."""
+        lines = open(os.path.join(self.profile, 'user.js'), 'r').read().splitlines()
+        s = lines.index('#MozRunner Prefs Start') ; e = lines.index('#MozRunner Prefs End')
+        cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:])
+        f = open(os.path.join(self.profile, 'user.js'), 'w')
+        f.write(cleaned_prefs) ; f.flush() ; f.close()
+
+    ### cleanup
+ 
+    def cleanup(self):
+        """Cleanup operations on the profile."""
+        if self.create_new:
+            if os.path.exists(self.profile):
+                rmtree(self.profile)
+        else:
+            self.clean_preferences()
+            self.clean_addons()
+
+    __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 app folder
+                   'extensions.enabledScopes' : 5,
+                   # 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,
+                   }
+
+
+def print_addon_ids(args=sys.argv[1:]):
+    """print addon ids for testing"""
+    for arg in args:
+        print Profile.addon_id(arg)
new file mode 100644
--- /dev/null
+++ b/testing/firebug/mozprofile/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 Mozilla Corporation Code.
+#
+# The Initial Developer of the Original Code is
+# Mikeal Rogers.
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Mikeal Rogers <mikeal.rogers@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 setuptools import setup, find_packages
+import sys
+
+version = '0.1a'
+
+# we only support python 2 right now
+assert sys.version_info[0] == 2
+
+deps = []
+# version-dependent dependencies
+if sys.version_info[1] < 6:
+    deps.append('simplejson')
+
+setup(name='mozprofile',
+      version=version,
+      description="handling of Mozilla XUL app profiles",
+      long_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]
+      addon_id = mozprofile:print_addon_ids
+      """,
+      )
new file mode 100644
--- /dev/null
+++ b/testing/firebug/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 Mozilla Corporation Code.
+#
+# The Initial Developer of the Original Code is
+# Mikeal Rogers.
+# 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/firebug/mozrunner/mozrunner/runner.py
@@ -0,0 +1,372 @@
+# ***** 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 Corporation Code.
+#
+# The Initial Developer of the Original Code is
+# Mikeal Rogers.
+# 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 *****
+
+__all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'create_runner', 'CLI', 'cli']
+
+import os
+import sys
+import signal
+import optparse
+import ConfigParser
+
+from utils import findInPath
+from mozprocess import killableprocess
+from mozprocess.pid import get_pids
+from mozprofile import *
+
+class Runner(object):
+    """Handles all running operations. Finds bins, runs and kills the process."""
+
+    def __init__(self, profile, binary=None, cmdargs=None, env=None, kp_kwargs=None):
+        self.process_handler = None
+        self.profile = profile
+                 
+        self.binary = self.__class__.get_binary(binary)
+
+        if not os.path.exists(self.binary):
+            raise Exception("Binary path does not exist "+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')
+
+        if env is None:
+            self.env = os.environ.copy()
+            self.env.update({'MOZ_NO_REMOTE':'1',})
+        else:
+            self.env = env
+        self.kp_kwargs = kp_kwargs or {}
+
+    @classmethod
+    def get_binary(cls, binary=None):
+        """determine the binary"""
+        if binary is None:
+            return cls.find_binary()
+        elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1:
+            # TODO FIX ME!!!
+            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 sys.platform in ('linux2', 'sunos5', 'solaris'):
+            for name in reversed(cls.names):
+                binary = findInPath(name)
+        elif os.name == 'nt' or sys.platform == 'cygwin':
+
+            # 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 reversed(cls.names):
+                binary = findInPath(name)
+                if sys.platform == 'cygwin':
+                    program_files = os.environ['PROGRAMFILES']
+                else:
+                    program_files = os.environ['ProgramFiles']
+
+                if binary is None:
+                    for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe'),
+                                (os.environ.get("ProgramFiles(x86)"),'Mozilla Firefox', 'firefox.exe'),
+                                (program_files,'Minefield', 'firefox.exe'),
+                                (os.environ.get("ProgramFiles(x86)"),'Minefield', 'firefox.exe')
+                                ]:
+                        path = os.path.join(*bin)
+                        if os.path.isfile(path):
+                            binary = path
+                            break
+        elif sys.platform == 'darwin':
+            for name in reversed(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 is None:
+            raise Exception('Mozrunner could not locate your binary, you will need to set it.')
+        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."""
+        # TODO: I think we should keep this, but I think Jeff's patch moves it to the top of the fileimport ConfigParser
+
+        config = ConfigParser.RawConfigParser()
+        dirname = os.path.dirname(self.binary)
+        repository = { }
+
+        for file, section in [('application', 'App'), ('platform', 'Build')]:
+            config.read(os.path.join(dirname, '%s.ini' % file))
+
+            for key, id in [('SourceRepository', 'repository'),
+                           ('SourceStamp', 'changeset')]:
+                try:
+                    repository['%s_%s' % (file, id)] = config.get(section, key);
+                except:
+                    repository['%s_%s' % (file, id)] = None
+
+        return repository
+
+    def start(self):
+        """Run self.command in the proper environment."""
+        self.process_handler = killableprocess.runCommand(self.command+self.cmdargs, env=self.env, **self.kp_kwargs)
+
+    def wait(self, timeout=None):
+        """Wait for the browser to exit."""
+        self.process_handler.wait(timeout=timeout)
+
+        if sys.platform != 'win32':
+            for name in self.names:
+                for pid in get_pids(name, self.process_handler.pid):
+                    self.process_handler.pid = pid
+                    self.process_handler.wait(timeout=timeout)
+
+    def stop(self):
+        """Kill the app"""
+        if self.process_handler is None:
+            return
+        
+        if sys.platform != 'win32':
+            self.process_handler.kill()
+            for name in self.names:
+                for pid in get_pids(name, self.process_handler.pid):
+                    self.process_handler.pid = pid
+                    self.process_handler.kill()
+        else:
+            try:
+                self.process_handler.kill(group=True)
+            except Exception, e:
+                raise Exception('Cannot kill process, '+type(e).__name__+' '+e.message)
+
+    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()
+        self.profile.cleanup()
+
+    __del__ = cleanup
+
+
+class FirefoxRunner(Runner):
+    """Specialized Runner subclass for running Firefox."""
+
+    app_name = 'Firefox'
+    profile_class = FirefoxProfile
+
+    if sys.platform == 'darwin':
+        names = ['firefox', 'minefield', 'shiretoko']
+    elif (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
+        names = ['firefox', 'mozilla-firefox', 'iceweasel']
+    elif os.name == 'nt' or sys.platform == 'cygwin':
+        names =['firefox']
+    else:
+        raise AssertionError("I don't know what platform you're on")
+
+class ThunderbirdRunner(Runner):
+    """Specialized Runner subclass for running Thunderbird"""
+    app_name = 'Thunderbird'
+
+    names = ["thunderbird", "shredder"]
+
+def create_runner(profile_class, runner_class,
+                  binary=None, profile_args=None, runner_args=None):
+    """Get the runner object, a not-very-abstract factory"""
+    profile_args = profile_args or {}
+    runner_args = runner_args or {}
+    profile = profile_class(**profile_args)
+    binary = runner_class.get_binary(binary)
+    runner = runner_class(binary=binary,
+                          profile=profile,
+                          **runner_args)
+    return runner    
+
+class CLI(object):
+    """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 = self.get_metadata_from_egg()
+        self.parser = optparse.OptionParser(version="%prog " + self.metadata["Version"])
+        self.add_options(self.parser)
+        (self.options, self.args) = self.parser.parse_args(args)
+
+        if self.options.info:
+            self.print_metadata()
+            sys.exit(0)
+
+        # choose appropriate runner and profile classes
+        if self.options.app == 'firefox':
+            self.runner_class = FirefoxRunner
+            self.profile_class = FirefoxProfile
+        elif self.options.app == 'thunderbird':
+            self.runner_class = ThunderbirdRunner
+            self.profile_class = ThunderbirdProfile
+        else:
+            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"""
+        
+        parser.add_option('-b', "--binary",
+                          dest="binary", help="Binary path.",
+                          metavar=None, default=None)
+        
+        parser.add_option('-p', "--profile",
+                         dest="profile", help="Profile path.",
+                         metavar=None, default=None)
+        
+        parser.add_option('-a', "--addon", dest="addons",
+                         action='append',
+                         help="Addons paths to install",
+                         metavar=None, default=[])
+        
+        parser.add_option("--info", dest="info", default=False,
+                          action="store_true",
+                          help="Print module information")
+        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")
+
+    ### methods regarding 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 profile_args(self):
+        """arguments to instantiate the profile class"""
+        return dict(profile=self.options.profile,
+                    addons=self.options.addons)
+
+    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())
+
+    def create_runner(self):
+        return create_runner(self.profile_class,
+                             self.runner_class,
+                             self.options.binary,
+                             self.profile_args(),
+                             self.runner_args())
+
+    def run(self):
+        runner = self.create_runner()
+        self.start(runner)
+        # XXX should be runner.cleanup,
+        # and other runner cleanup code should go in there
+        runner.profile.cleanup()
+
+    def start(self, runner):
+        """Starts the runner and waits for Firefox to exitor Keyboard Interrupt.
+        Shoule be overwritten to provide custom running of the runner instance."""
+        runner.start()
+        print 'Started:', ' '.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/firebug/mozrunner/mozrunner/utils.py
@@ -0,0 +1,62 @@
+#!/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 Corporation Code.
+#
+# The Initial Developer of the Original Code is
+# Mikeal Rogers.
+# 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 *****
+
+"""
+utility functions for mozrunner
+"""
+
+__all__ = ['findInPath']
+
+import os
+import sys
+
+def findInPath(fileName, path=os.environ['PATH']):
+    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 os.name == 'nt' or sys.platform == 'cygwin':
+            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/firebug/mozrunner/setup.py
@@ -0,0 +1,75 @@
+# ***** 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 Corporation Code.
+#
+# The Initial Developer of the Original Code is
+# Mikeal Rogers.
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#  Mikeal Rogers <mikeal.rogers@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 setuptools import setup, find_packages
+import sys
+
+desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
+
+
+PACKAGE_NAME = "mozrunner"
+PACKAGE_VERSION = "3.0a"
+
+deps = ['mozprocess', 'mozprofile']
+
+# we only support python 2 right now
+assert sys.version_info[0] == 2
+
+setup(name=PACKAGE_NAME,
+      version=PACKAGE_VERSION,
+      description=desc,
+      long_description=desc,
+      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',
+                  ]
+     )
--- a/testing/testsuite-targets.mk
+++ b/testing/testsuite-targets.mk
@@ -194,17 +194,17 @@ mozmill-all:
 	$(MAKE) mozmill TEST_PATH=$(MOZMILL_TEST_PATH)
 	$(MAKE) mozmill-restart TEST_PATH=$(MOZMILL_RESTART_TEST_PATH)
 
 # 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-mozmill stage-jetpack
+package-tests: stage-mochitest stage-reftest stage-xpcshell stage-jstests stage-mozmill stage-jetpack stage-firebug
 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
@@ -216,17 +216,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
+	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
 
 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
@@ -238,14 +238,17 @@ stage-jstests: make-stage-dir
 stage-mozmill: make-stage-dir
 	$(MAKE) -C $(DEPTH)/testing/mozmill stage-package
 
 stage-android: make-stage-dir
 	$(NSINSTALL) $(DEPTH)/build/mobile/sutagent/android/sutAgentAndroid.apk $(PKG_STAGE)/bin
 
 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
 .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-mozmill stage-android stage-jetpack
+  package-tests make-stage-dir stage-mochitest stage-reftest stage-xpcshell stage-jstests stage-mozmill stage-android stage-jetpack stage-firebug
--- a/toolkit/toolkit-makefiles.sh
+++ b/toolkit/toolkit-makefiles.sh
@@ -941,16 +941,17 @@ if [ "$ENABLE_TESTS" ]; then
     testing/mochitest/tests/MochiKit-1.4.2/MochiKit/Makefile
     testing/mochitest/tests/MochiKit-1.4.2/tests/Makefile
     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
     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