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 idunknown
push userunknown
push dateunknown
reviewersctalbert, NPOTB
bugs631659
milestone2.0b13pre
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