Bug 530475, convert test harness python code to classes for future integration with mobile framework. r=ted patch=jmaher
authorJoel Maher <jmaher@mozilla.com>
Wed, 13 Jan 2010 13:53:26 -0800
changeset 37159 745af1f3dbf5bdb93a95e38d0a534ba489754cc8
parent 37158 1ef1d73eb050f79d35b528150c425022ee61f0ba
child 37160 a6ce37b09cf51cce00745096ee5606706e239443
child 37161 b5d9fbeec04ed773b9f8c23973fe0e16e301343b
push idunknown
push userunknown
push dateunknown
reviewersted
bugs530475
milestone1.9.3a1pre
Bug 530475, convert test harness python code to classes for future integration with mobile framework. r=ted patch=jmaher
build/automation.py.in
build/leaktest.py.in
build/pgo/genpgocert.py.in
build/pgo/profileserver.py.in
layout/tools/reftest/runreftest.py
testing/mochitest/runtests.py.in
testing/xpcshell/runxpcshelltests.py
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -46,107 +46,35 @@ import re
 import select
 import shutil
 import signal
 import subprocess
 import sys
 import threading
 import tempfile
 
-"""
-Runs the browser from a script, and provides useful utilities
-for setting up the browser environment.
-"""
-
-SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
-sys.path.insert(0, SCRIPT_DIR);
-from automationutils import checkForCrashes
-
-__all__ = [
-           "UNIXISH",
-           "IS_WIN32",
-           "IS_MAC",
-           "log",
-           "runApp",
-           "Process",
-           "addExtraCommonOptions",
-           "initializeProfile",
-           "DIST_BIN",
-           "DEFAULT_APP",
-           "CERTS_SRC_DIR",
-           "environment",
-           "IS_TEST_BUILD",
-           "IS_DEBUG_BUILD",
-           "DEFAULT_TIMEOUT",
-          ]
-
-# timeout, in seconds
-DEFAULT_TIMEOUT = 60.0
-
-# These are generated in mozilla/build/Makefile.in
-#expand DIST_BIN = __XPC_BIN_PATH__
-#expand IS_WIN32 = len("__WIN32__") != 0
-#expand IS_MAC = __IS_MAC__ != 0
-#expand IS_LINUX = __IS_LINUX__ != 0
-#ifdef IS_CYGWIN
-#expand IS_CYGWIN = __IS_CYGWIN__ == 1
-#else
-IS_CYGWIN = False
-#endif
-#expand IS_CAMINO = __IS_CAMINO__ != 0
-#expand BIN_SUFFIX = __BIN_SUFFIX__
-#expand PERL = __PERL__
-
-UNIXISH = not IS_WIN32 and not IS_MAC
 
-#expand DEFAULT_APP = "./" + __BROWSER_PATH__
-#expand CERTS_SRC_DIR = __CERTS_SRC_DIR__
-#expand IS_TEST_BUILD = __IS_TEST_BUILD__
-#expand IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
-#expand CRASHREPORTER = __CRASHREPORTER__ == 1
-
-###########
-# LOGGING #
-###########
-
-# We use the logging system here primarily because it'll handle multiple
-# threads, which is needed to process the output of the server and application
-# processes simultaneously.
-log = logging.getLogger()
-handler = logging.StreamHandler(sys.stdout)
-log.setLevel(logging.INFO)
-log.addHandler(handler)
-
-
-#################
-# SUBPROCESSING #
-#################
+#expand _DIST_BIN = __XPC_BIN_PATH__
+#expand _IS_WIN32 = len("__WIN32__") != 0
+#expand _IS_MAC = __IS_MAC__ != 0
+#expand _IS_LINUX = __IS_LINUX__ != 0
+#ifdef IS_CYGWIN
+#expand _IS_CYGWIN = __IS_CYGWIN__ == 1
+#else
+_IS_CYGWIN = False
+#endif
+#expand _IS_CAMINO = __IS_CAMINO__ != 0
+#expand _BIN_SUFFIX = __BIN_SUFFIX__
+#expand _PERL = __PERL__
 
-class Process(subprocess.Popen):
-  """
-  Represents our view of a subprocess.
-  It adds a kill() method which allows it to be stopped explicitly.
-  """
-
-  def kill(self):
-    if IS_WIN32:
-      import platform
-      pid = "%i" % self.pid
-      if platform.release() == "2000":
-        # Windows 2000 needs 'kill.exe' from the 'Windows 2000 Resource Kit tools'. (See bug 475455.)
-        try:
-          subprocess.Popen(["kill", "-f", pid]).wait()
-        except:
-          log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
-      else:
-        # Windows XP and later.
-        subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
-    else:
-      os.kill(self.pid, signal.SIGKILL)
-
+#expand _DEFAULT_APP = "./" + __BROWSER_PATH__
+#expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
+#expand _IS_TEST_BUILD = __IS_TEST_BUILD__
+#expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
+#expand _CRASHREPORTER = __CRASHREPORTER__ == 1
 
 #################
 # PROFILE SETUP #
 #################
 
 class SyntaxError(Exception):
   "Signifies a syntax error on a particular line in server-locations.txt."
 
@@ -167,83 +95,166 @@ class Location:
   "Represents a location line in server-locations.txt."
 
   def __init__(self, scheme, host, port, options):
     self.scheme = scheme
     self.host = host
     self.port = port
     self.options = options
 
-
-def readLocations(locationsPath = "server-locations.txt"):
+class Automation(object):
   """
-  Reads the locations at which the Mochitest HTTP server is available from
-  server-locations.txt.
+  Runs the browser from a script, and provides useful utilities
+  for setting up the browser environment.
   """
 
-  locationFile = codecs.open(locationsPath, "r", "UTF-8")
+  DIST_BIN = _DIST_BIN
+  IS_WIN32 = _IS_WIN32
+  IS_MAC = _IS_MAC
+  IS_LINUX = _IS_LINUX
+  IS_CYGWIN = _IS_CYGWIN
+  IS_CAMINO = _IS_CAMINO
+  BIN_SUFFIX = _BIN_SUFFIX
+  PERL = _PERL
+
+  UNIXISH = not IS_WIN32 and not IS_MAC
+
+  DEFAULT_APP = _DEFAULT_APP
+  CERTS_SRC_DIR = _CERTS_SRC_DIR
+  IS_TEST_BUILD = _IS_TEST_BUILD
+  IS_DEBUG_BUILD = _IS_DEBUG_BUILD
+  CRASHREPORTER = _CRASHREPORTER
+
+  SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
+  sys.path.insert(0, SCRIPT_DIR)
+  automationutils = __import__('automationutils')
+
+  # timeout, in seconds
+  DEFAULT_TIMEOUT = 60.0
+
+  log = logging.getLogger()
+
+  def __init__(self):
+
+    # We use the logging system here primarily because it'll handle multiple
+    # threads, which is needed to process the output of the server and application
+    # processes simultaneously.
+    handler = logging.StreamHandler(sys.stdout)
+    self.log.setLevel(logging.INFO)
+    self.log.addHandler(handler)
 
-  # Perhaps more detail than necessary, but it's the easiest way to make sure
-  # we get exactly the format we want.  See server-locations.txt for the exact
-  # format guaranteed here.
-  lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
+  @property
+  def __all__(self):
+    return [
+           "UNIXISH",
+           "IS_WIN32",
+           "IS_MAC",
+           "log",
+           "runApp",
+           "Process",
+           "addCommonOptions",
+           "initializeProfile",
+           "DIST_BIN",
+           "DEFAULT_APP",
+           "CERTS_SRC_DIR",
+           "environment",
+           "IS_TEST_BUILD",
+           "IS_DEBUG_BUILD",
+           "DEFAULT_TIMEOUT",
+          ]
+
+  class Process(subprocess.Popen):
+    """
+    Represents our view of a subprocess.
+    It adds a kill() method which allows it to be stopped explicitly.
+    """
+
+    def kill(self):
+      if Automation().IS_WIN32:
+        import platform
+        pid = "%i" % self.pid
+        if platform.release() == "2000":
+          # Windows 2000 needs 'kill.exe' from the 
+          #'Windows 2000 Resource Kit tools'. (See bug 475455.)
+          try:
+            subprocess.Popen(["kill", "-f", pid]).wait()
+          except:
+            self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
+        else:
+          # Windows XP and later.
+          subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
+      else:
+        os.kill(self.pid, signal.SIGKILL)
+
+  def readLocations(self, locationsPath = "server-locations.txt"):
+    """
+    Reads the locations at which the Mochitest HTTP server is available from
+    server-locations.txt.
+    """
+
+    locationFile = codecs.open(locationsPath, "r", "UTF-8")
+
+    # Perhaps more detail than necessary, but it's the easiest way to make sure
+    # we get exactly the format we want.  See server-locations.txt for the exact
+    # format guaranteed here.
+    lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
                       r"://"
                       r"(?P<host>"
                         r"\d+\.\d+\.\d+\.\d+"
                         r"|"
                         r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
                         r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
                       r")"
                       r":"
                       r"(?P<port>\d+)"
                       r"(?:"
                       r"\s+"
                       r"(?P<options>\S+(?:,\S+)*)"
                       r")?$")
-  locations = []
-  lineno = 0
-  seenPrimary = False
-  for line in locationFile:
-    lineno += 1
-    if line.startswith("#") or line == "\n":
-      continue
+    locations = []
+    lineno = 0
+    seenPrimary = False
+    for line in locationFile:
+      lineno += 1
+      if line.startswith("#") or line == "\n":
+        continue
       
-    match = lineRe.match(line)
-    if not match:
-      raise SyntaxError(lineno)
+      match = lineRe.match(line)
+      if not match:
+        raise SyntaxError(lineno)
 
-    options = match.group("options")
-    if options:
-      options = options.split(",")
-      if "primary" in options:
-        if seenPrimary:
-          raise SyntaxError(lineno, "multiple primary locations")
-        seenPrimary = True
-    else:
-      options = []
+      options = match.group("options")
+      if options:
+        options = options.split(",")
+        if "primary" in options:
+          if seenPrimary:
+            raise SyntaxError(lineno, "multiple primary locations")
+          seenPrimary = True
+      else:
+        options = []
 
-    locations.append(Location(match.group("scheme"), match.group("host"),
-                              match.group("port"), options))
+      locations.append(Location(match.group("scheme"), match.group("host"),
+                                match.group("port"), options))
 
-  if not seenPrimary:
-    raise SyntaxError(lineno + 1, "missing primary location")
+    if not seenPrimary:
+      raise SyntaxError(lineno + 1, "missing primary location")
 
-  return locations
+    return locations
 
 
-def initializeProfile(profileDir, extraPrefs = []):
-  "Sets up the standard testing profile."
+  def initializeProfile(self, profileDir, extraPrefs = []):
+    "Sets up the standard testing profile."
 
-  # Start with a clean slate.
-  shutil.rmtree(profileDir, True)
-  os.mkdir(profileDir)
+    # Start with a clean slate.
+    shutil.rmtree(profileDir, True)
+    os.mkdir(profileDir)
 
-  prefs = []
+    prefs = []
 
-  part = """\
+    part = """\
 user_pref("browser.dom.window.dump.enabled", true);
 user_pref("dom.allow_scripts_to_close_windows", true);
 user_pref("dom.disable_open_during_load", false);
 user_pref("dom.max_script_run_time", 0); // no slow script dialogs
 user_pref("dom.max_chrome_script_run_time", 0);
 user_pref("dom.popup_maximum", -1);
 user_pref("signed.applets.codebase_principal_support", true);
 user_pref("security.warn_submit_insecure", false);
@@ -272,40 +283,40 @@ user_pref("camino.warn_when_closing", fa
 user_pref("urlclassifier.updateinterval", 172800);
 // Point the url-classifier to the local testing server for fast failures
 user_pref("browser.safebrowsing.provider.0.gethashURL", "http://localhost:8888/safebrowsing-dummy/gethash");
 user_pref("browser.safebrowsing.provider.0.keyURL", "http://localhost:8888/safebrowsing-dummy/newkey");
 user_pref("browser.safebrowsing.provider.0.lookupURL", "http://localhost:8888/safebrowsing-dummy/lookup");
 user_pref("browser.safebrowsing.provider.0.updateURL", "http://localhost:8888/safebrowsing-dummy/update");
 """
   
-  prefs.append(part)
+    prefs.append(part)
 
-  locations = readLocations()
+    locations = self.readLocations()
 
-  # Grant God-power to all the privileged servers on which tests run.
-  privileged = filter(lambda loc: "privileged" in loc.options, locations)
-  for (i, l) in itertools.izip(itertools.count(1), privileged):
-    part = """
+    # Grant God-power to all the privileged servers on which tests run.
+    privileged = filter(lambda loc: "privileged" in loc.options, locations)
+    for (i, l) in itertools.izip(itertools.count(1), privileged):
+      part = """
 user_pref("capability.principal.codebase.p%(i)d.granted",
           "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
            UniversalPreferencesRead UniversalPreferencesWrite \
            UniversalFileRead");
 user_pref("capability.principal.codebase.p%(i)d.id", "%(origin)s");
 user_pref("capability.principal.codebase.p%(i)d.subjectName", "");
 """  % { "i": i,
          "origin": (l.scheme + "://" + l.host + ":" + l.port) }
-    prefs.append(part)
+      prefs.append(part)
 
-  # We need to proxy every server but the primary one.
-  origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
-             for l in filter(lambda l: "primary" not in l.options, locations)]
-  origins = ", ".join(origins)
+    # We need to proxy every server but the primary one.
+    origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
+              for l in filter(lambda l: "primary" not in l.options, locations)]
+    origins = ", ".join(origins)
 
-  pacURL = """data:text/plain,
+    pacURL = """data:text/plain,
 function FindProxyForURL(url, host)
 {
   var origins = [%(origins)s];
   var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
                          '://' +
                          '(?:[^/@]*@)?' +
                          '(.*?)' +
                          '(?::(\\\\\\\\d+))?/');
@@ -324,387 +335,401 @@ function FindProxyForURL(url, host)
   if (origins.indexOf(origin) < 0)
     return 'DIRECT';
   if (isHttp)
     return 'PROXY 127.0.0.1:8888';
   if (isHttps)
     return 'PROXY 127.0.0.1:4443';
   return 'DIRECT';
 }""" % { "origins": origins }
-  pacURL = "".join(pacURL.splitlines())
+    pacURL = "".join(pacURL.splitlines())
 
-  part = """
+    part = """
 user_pref("network.proxy.type", 2);
 user_pref("network.proxy.autoconfig_url", "%(pacURL)s");
 
 user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
 """ % {"pacURL": pacURL}
-  prefs.append(part)
-
-  for v in extraPrefs:
-    thispref = v.split("=")
-    if len(thispref) < 2:
-      print "Error: syntax error in --setpref=" + v
-      sys.exit(1)
-    part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
     prefs.append(part)
 
-  # write the preferences
-  prefsFile = open(profileDir + "/" + "user.js", "a")
-  prefsFile.write("".join(prefs))
-  prefsFile.close()
+    for v in extraPrefs:
+      thispref = v.split("=")
+      if len(thispref) < 2:
+        print "Error: syntax error in --setpref=" + v
+        sys.exit(1)
+      part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
+      prefs.append(part)
 
-def addExtraCommonOptions(parser):
-  "Adds command-line options which are common to mochitest and reftest."
-
-  parser.add_option("--setpref",
-                    action = "append", type = "string",
-                    default = [],
-                    dest = "extraPrefs", metavar = "PREF=VALUE",
-                    help = "defines an extra user preference")  
+    # write the preferences
+    prefsFile = open(profileDir + "/" + "user.js", "a")
+    prefsFile.write("".join(prefs))
+    prefsFile.close()
 
-def fillCertificateDB(profileDir, certPath, utilityPath, xrePath):
-  pwfilePath = os.path.join(profileDir, ".crtdbpw")
-  
-  pwfile = open(pwfilePath, "w")
-  pwfile.write("\n")
-  pwfile.close()
+  def addCommonOptions(self, parser):
+    "Adds command-line options which are common to mochitest and reftest."
 
-  # Create head of the ssltunnel configuration file
-  sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
-  sslTunnelConfig = open(sslTunnelConfigPath, "w")
+    parser.add_option("--setpref",
+                      action = "append", type = "string",
+                      default = [],
+                      dest = "extraPrefs", metavar = "PREF=VALUE",
+                      help = "defines an extra user preference")  
+
+  def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
+    pwfilePath = os.path.join(profileDir, ".crtdbpw")
   
-  sslTunnelConfig.write("httpproxy:1\n")
-  sslTunnelConfig.write("certdbdir:%s\n" % certPath)
-  sslTunnelConfig.write("forward:127.0.0.1:8888\n")
-  sslTunnelConfig.write("listen:*:4443:pgo server certificate\n")
+    pwfile = open(pwfilePath, "w")
+    pwfile.write("\n")
+    pwfile.close()
 
-  # Configure automatic certificate and bind custom certificates, client authentication
-  locations = readLocations()
-  locations.pop(0)
-  for loc in locations:
-    if loc.scheme == "https" and "nocert" not in loc.options:
-      customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
-      clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
-      for option in loc.options:
-        match = customCertRE.match(option)
-        if match:
-          customcert = match.group("nickname");
-          sslTunnelConfig.write("listen:%s:%s:4443:%s\n" %
-              (loc.host, loc.port, customcert))
+    # Create head of the ssltunnel configuration file
+    sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
+    sslTunnelConfig = open(sslTunnelConfigPath, "w")
+  
+    sslTunnelConfig.write("httpproxy:1\n")
+    sslTunnelConfig.write("certdbdir:%s\n" % certPath)
+    sslTunnelConfig.write("forward:127.0.0.1:8888\n")
+    sslTunnelConfig.write("listen:*:4443:pgo server certificate\n")
 
-        match = clientAuthRE.match(option)
-        if match:
-          clientauth = match.group("clientauth");
-          sslTunnelConfig.write("clientauth:%s:%s:4443:%s\n" %
-              (loc.host, loc.port, clientauth))
-
-  sslTunnelConfig.close()
+    # Configure automatic certificate and bind custom certificates, client authentication
+    locations = self.readLocations()
+    locations.pop(0)
+    for loc in locations:
+      if loc.scheme == "https" and "nocert" not in loc.options:
+        customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
+        clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
+        for option in loc.options:
+          match = customCertRE.match(option)
+          if match:
+            customcert = match.group("nickname");
+            sslTunnelConfig.write("listen:%s:%s:4443:%s\n" %
+                      (loc.host, loc.port, customcert))
 
-  # Pre-create the certification database for the profile
-  env = environment(xrePath = xrePath)
-  certutil = os.path.join(utilityPath, "certutil" + BIN_SUFFIX)
-  pk12util = os.path.join(utilityPath, "pk12util" + BIN_SUFFIX)
+          match = clientAuthRE.match(option)
+          if match:
+            clientauth = match.group("clientauth");
+            sslTunnelConfig.write("clientauth:%s:%s:4443:%s\n" %
+                      (loc.host, loc.port, clientauth))
+
+    sslTunnelConfig.close()
 
-  status = Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
-  if status != 0:
-    return status
+    # Pre-create the certification database for the profile
+    env = self.environment(xrePath = xrePath)
+    certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
+    pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
+
+    status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
+    if status != 0:
+      return status
 
-  # Walk the cert directory and add custom CAs and client certs
-  files = os.listdir(certPath)
-  for item in files:
-    root, ext = os.path.splitext(item)
-    if ext == ".ca":
-      trustBits = "CT,,"
-      if root.endswith("-object"):
-        trustBits = "CT,,CT"
-      Process([certutil, "-A", "-i", os.path.join(certPath, item),
-        "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
-        env = env).wait()
-    if ext == ".client":
-      Process([pk12util, "-i", os.path.join(certPath, item), "-w",
-        pwfilePath, "-d", profileDir], 
-        env = env).wait()
+    # Walk the cert directory and add custom CAs and client certs
+    files = os.listdir(certPath)
+    for item in files:
+      root, ext = os.path.splitext(item)
+      if ext == ".ca":
+        trustBits = "CT,,"
+        if root.endswith("-object"):
+          trustBits = "CT,,CT"
+        self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
+                    "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
+                    env = env).wait()
+      if ext == ".client":
+        self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
+                    pwfilePath, "-d", profileDir], 
+                    env = env).wait()
+
+    os.unlink(pwfilePath)
+    return 0
 
-  os.unlink(pwfilePath)
-  return 0
-
-def environment(env = None, xrePath = DIST_BIN, crashreporter = True):
-  if env == None:
-    env = dict(os.environ)
+  def environment(self, env = None, xrePath = None, crashreporter = True):
+    if xrePath == None:
+      xrePath = self.DIST_BIN
+    if env == None:
+      env = dict(os.environ)
 
-  ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
-  if UNIXISH or IS_MAC:
-    envVar = "LD_LIBRARY_PATH"
-    if IS_MAC:
-      envVar = "DYLD_LIBRARY_PATH"
-    else: # unixish
-      env['MOZILLA_FIVE_HOME'] = xrePath
-    if envVar in env:
-      ldLibraryPath = ldLibraryPath + ":" + env[envVar]
-    env[envVar] = ldLibraryPath
-  elif IS_WIN32:
-    env["PATH"] = env["PATH"] + ";" + ldLibraryPath
+    ldLibraryPath = os.path.abspath(os.path.join(self.SCRIPT_DIR, xrePath))
+    if self.UNIXISH or self.IS_MAC:
+      envVar = "LD_LIBRARY_PATH"
+      if self.IS_MAC:
+        envVar = "DYLD_LIBRARY_PATH"
+      else: # unixish
+        env['MOZILLA_FIVE_HOME'] = xrePath
+      if envVar in env:
+        ldLibraryPath = ldLibraryPath + ":" + env[envVar]
+      env[envVar] = ldLibraryPath
+    elif self.IS_WIN32:
+      env["PATH"] = env["PATH"] + ";" + ldLibraryPath
 
-  if crashreporter:
-    env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
-    env['MOZ_CRASHREPORTER'] = '1'
-  else:
-    env['MOZ_CRASHREPORTER_DISABLE'] = '1'
+    if crashreporter:
+      env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
+      env['MOZ_CRASHREPORTER'] = '1'
+    else:
+      env['MOZ_CRASHREPORTER_DISABLE'] = '1'
 
-  env['GNOME_DISABLE_CRASH_DIALOG'] = "1"
-  env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
-  return env
+    env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
+    env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
+    return env
 
-if IS_WIN32:
-  import ctypes, ctypes.wintypes, time, msvcrt
-  PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
-  GetLastError = ctypes.windll.kernel32.GetLastError
+  if IS_WIN32:
+    ctypes = __import__('ctypes')
+    wintypes = __import__('ctypes.wintypes')
+    time = __import__('time')
+    msvcrt = __import__('msvcrt')
+    PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
+    GetLastError = ctypes.windll.kernel32.GetLastError
 
-  def readWithTimeout(f, timeout):
-    """Try to read a line of output from the file object |f|.
-    |f| must be a  pipe, like the |stdout| member of a subprocess.Popen
-    object created with stdout=PIPE. If no output
-    is received within |timeout| seconds, return a blank line.
-    Returns a tuple (line, did_timeout), where |did_timeout| is True
-    if the read timed out, and False otherwise."""
-    if timeout is None:
-      # shortcut to allow callers to pass in "None" for no timeout.
-      return (f.readline(), False)
-    x = msvcrt.get_osfhandle(f.fileno())
-    l = ctypes.c_long()
-    done = time.time() + timeout
-    while time.time() < done:
-      if PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
-        err = GetLastError()
-        if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
-          return ('', False)
-        else:
-          log.error("readWithTimeout got error: %d", err)
-      if l > 0:
-        # we're assuming that the output is line-buffered,
-        # which is not unreasonable
+    def readWithTimeout(self, f, timeout):
+      """Try to read a line of output from the file object |f|.
+      |f| must be a  pipe, like the |stdout| member of a subprocess.Popen
+      object created with stdout=PIPE. If no output
+      is received within |timeout| seconds, return a blank line.
+      Returns a tuple (line, did_timeout), where |did_timeout| is True
+      if the read timed out, and False otherwise."""
+      if timeout is None:
+        # shortcut to allow callers to pass in "None" for no timeout.
         return (f.readline(), False)
-      time.sleep(0.01)
-    return ('', True)
+      x = self.msvcrt.get_osfhandle(f.fileno())
+      l = self.ctypes.c_long()
+      done = self.time.time() + timeout
+      while self.time.time() < done:
+        if self.PeekNamedPipe(x, None, 0, None, self.ctypes.byref(l), None) == 0:
+          err = self.GetLastError()
+          if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
+            return ('', False)
+          else:
+            log.error("readWithTimeout got error: %d", err)
+        if l > 0:
+          # we're assuming that the output is line-buffered,
+          # which is not unreasonable
+          return (f.readline(), False)
+        self.time.sleep(0.01)
+      return ('', True)
 
-  def isPidAlive(pid):
-    STILL_ACTIVE = 259
-    PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
-    pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
-    if not pHandle:
-      return False
-    pExitCode = ctypes.wintypes.DWORD()
-    ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
-    ctypes.windll.kernel32.CloseHandle(pHandle)
-    if (pExitCode.value == STILL_ACTIVE):
-      return True
-    else:
-      return False
+    def isPidAlive(self, pid):
+      STILL_ACTIVE = 259
+      PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
+      pHandle = self.ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
+      if not pHandle:
+        return False
+      pExitCode = self.wintypes.DWORD()
+      self.ctypes.windll.kernel32.GetExitCodeProcess(pHandle, self.ctypes.byref(pExitCode))
+      self.ctypes.windll.kernel32.CloseHandle(pHandle)
+      if (pExitCode.value == STILL_ACTIVE):
+        return True
+      else:
+        return False
 
-  def killPid(pid):
-    PROCESS_TERMINATE = 0x0001
-    pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
-    if not pHandle:
-      return
-    success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
-    ctypes.windll.kernel32.CloseHandle(pHandle)
+    def killPid(self, pid):
+      PROCESS_TERMINATE = 0x0001
+      pHandle = self.ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
+      if not pHandle:
+        return
+      success = self.ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
+      self.ctypes.windll.kernel32.CloseHandle(pHandle)
 
-else:
-  import errno
-
-  def readWithTimeout(f, timeout):
-    """Try to read a line of output from the file object |f|. If no output
-    is received within |timeout| seconds, return a blank line.
-    Returns a tuple (line, did_timeout), where |did_timeout| is True
-    if the read timed out, and False otherwise."""
-    (r, w, e) = select.select([f], [], [], timeout)
-    if len(r) == 0:
-      return ('', True)
-    return (f.readline(), False)
+  else:
+    errno = __import__('errno')
 
-  def isPidAlive(pid):
-    try:
-      # kill(pid, 0) checks for a valid PID without actually sending a signal
-      # The method throws OSError if the PID is invalid, which we catch below.
-      os.kill(pid, 0)
+    def readWithTimeout(self, f, timeout):
+      """Try to read a line of output from the file object |f|. If no output
+      is received within |timeout| seconds, return a blank line.
+      Returns a tuple (line, did_timeout), where |did_timeout| is True
+      if the read timed out, and False otherwise."""
+      (r, w, e) = select.select([f], [], [], timeout)
+      if len(r) == 0:
+        return ('', True)
+      return (f.readline(), False)
 
-      # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
-      # the process terminates before we get to this point.
-      wpid, wstatus = os.waitpid(pid, os.WNOHANG)
-      if wpid == 0:
-        return True
+    def isPidAlive(self, pid):
+      try:
+        # kill(pid, 0) checks for a valid PID without actually sending a signal
+        # The method throws OSError if the PID is invalid, which we catch below.
+        os.kill(pid, 0)
 
-      return False
-    except OSError, err:
-      # Catch the errors we might expect from os.kill/os.waitpid, 
-      # and re-raise any others
-      if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
+        # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
+        # the process terminates before we get to this point.
+        wpid, wstatus = os.waitpid(pid, os.WNOHANG)
+        if wpid == 0:
+          return True
+
         return False
-      raise
-
-  def killPid(pid):
-    os.kill(pid, signal.SIGKILL)
+      except OSError, err:
+        # Catch the errors we might expect from os.kill/os.waitpid, 
+        # and re-raise any others
+        if err.errno == self.errno.ESRCH or err.errno == self.errno.ECHILD:
+          return False
+        raise
 
-def triggerBreakpad(proc, utilityPath):
-  """Attempt to kill this process in a way that triggers Breakpad crash
-  reporting, if we know how for this platform. Otherwise just .kill() it."""
-  if CRASHREPORTER:
-    if UNIXISH:
-      # SEGV will get picked up by Breakpad's signal handler
-      os.kill(proc.pid, signal.SIGSEGV)
-      return
-    elif IS_WIN32:
-      # We should have a "crashinject" program in our utility path
-      crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
-      if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
+    def killPid(self, pid):
+      os.kill(pid, signal.SIGKILL)
+
+  def triggerBreakpad(self, proc, utilityPath):
+    """Attempt to kill this process in a way that triggers Breakpad crash
+    reporting, if we know how for this platform. Otherwise just .kill() it."""
+    if self.CRASHREPORTER:
+      if self.UNIXISH:
+        # SEGV will get picked up by Breakpad's signal handler
+        os.kill(proc.pid, signal.SIGSEGV)
         return
-  #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
-  log.info("Can't trigger Breakpad, just killing process")
-  proc.kill()
-
-###############
-# RUN THE APP #
-###############
+      elif self.IS_WIN32:
+        # We should have a "crashinject" program in our utility path
+        crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
+        if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
+          return
+    #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
+    self.log.info("Can't trigger Breakpad, just killing process")
+    proc.kill()
 
-def runApp(testURL, env, app, profileDir, extraArgs,
-           runSSLTunnel = False, utilityPath = DIST_BIN,
-           xrePath = DIST_BIN, certPath = CERTS_SRC_DIR,
-           debuggerInfo = None, symbolsPath = None,
-           timeout = DEFAULT_TIMEOUT, maxTime = None):
-  """
-  Run the app, log the duration it took to execute, return the status code.
-  Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
-  """
+  def runApp(self, testURL, env, app, profileDir, extraArgs,
+             runSSLTunnel = False, utilityPath = None,
+             xrePath = None, certPath = None,
+             debuggerInfo = None, symbolsPath = None,
+             timeout = -1, maxTime = None):
+    """
+    Run the app, log the duration it took to execute, return the status code.
+    Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
+    """
 
-  # copy env so we don't munge the caller's environment
-  env = dict(env);
-  env["NO_EM_RESTART"] = "1"
-  tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
-  os.close(tmpfd)
-  env["MOZ_PROCESS_LOG"] = processLog
+    if utilityPath == None:
+      utilityPath = self.DIST_BIN
+    if xrePath == None:
+      xrePath = self.DIST_BIN
+    if certPath == None:
+      certPath = self.CERTS_SRC_DIR
+    if timeout == -1:
+      timeout = self.DEFAULT_TIMEOUT
 
-  if IS_TEST_BUILD and runSSLTunnel:
-    # create certificate database for the profile
-    certificateStatus = fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
-    if certificateStatus != 0:
-      log.info("TEST-UNEXPECTED FAIL | automation.py | Certificate integration failed")
-      return certificateStatus
+    # copy env so we don't munge the caller's environment
+    env = dict(env);
+    env["NO_EM_RESTART"] = "1"
+    tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
+    os.close(tmpfd)
+    env["MOZ_PROCESS_LOG"] = processLog
 
-    # start ssltunnel to provide https:// URLs capability
-    ssltunnel = os.path.join(utilityPath, "ssltunnel" + BIN_SUFFIX)
-    ssltunnelProcess = Process([ssltunnel, os.path.join(profileDir, "ssltunnel.cfg")], env = environment(xrePath = xrePath))
-    log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
+    if self.IS_TEST_BUILD and runSSLTunnel:
+      # create certificate database for the profile
+      certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
+      if certificateStatus != 0:
+        self.log.info("TEST-UNEXPECTED FAIL | automation.py | Certificate integration failed")
+        return certificateStatus
 
-  # now run with the profile we created
-  cmd = app
-  if IS_MAC and not IS_CAMINO and not cmd.endswith("-bin"):
-    cmd += "-bin"
-  cmd = os.path.abspath(cmd)
-
-  args = []
+      # start ssltunnel to provide https:// URLs capability
+      ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
+      ssltunnelProcess = self.Process([ssltunnel, 
+                               os.path.join(profileDir, "ssltunnel.cfg")], 
+                               env = self.environment(xrePath = xrePath))
+      self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
 
-  if debuggerInfo:
-    args.extend(debuggerInfo["args"])
-    args.append(cmd)
-    cmd = os.path.abspath(debuggerInfo["path"])
+    # now run with the profile we created
+    cmd = app
+    if self.IS_MAC and not self.IS_CAMINO and not cmd.endswith("-bin"):
+      cmd += "-bin"
+    cmd = os.path.abspath(cmd)
+
+    args = []
 
-  if IS_MAC:
-    args.append("-foreground")
+    if debuggerInfo:
+      args.extend(debuggerInfo["args"])
+      args.append(cmd)
+      cmd = os.path.abspath(debuggerInfo["path"])
+
+    if self.IS_MAC:
+      args.append("-foreground")
 
-  if IS_CYGWIN:
-    profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
-  else:
-    profileDirectory = profileDir + "/"
-
-  args.extend(("-no-remote", "-profile", profileDirectory))
-  if testURL is not None:
-    if IS_CAMINO:
-      args.extend(("-url", testURL))
+    if self.IS_CYGWIN:
+      profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
     else:
-      args.append((testURL))
-  args.extend(extraArgs)
+      profileDirectory = profileDir + "/"
 
-  startTime = datetime.now()
+    args.extend(("-no-remote", "-profile", profileDirectory))
+    if testURL is not None:
+      if self.IS_CAMINO:
+        args.extend(("-url", testURL))
+      else:
+        args.append((testURL))
+    args.extend(extraArgs)
 
-  # Don't redirect stdout and stderr if an interactive debugger is attached
-  if debuggerInfo and debuggerInfo["interactive"]:
-    outputPipe = None
-  else:
-    outputPipe = subprocess.PIPE
+    startTime = datetime.now()
 
-  proc = Process([cmd] + args,
-                 env = environment(env, xrePath = xrePath,
+    # Don't redirect stdout and stderr if an interactive debugger is attached
+    if debuggerInfo and debuggerInfo["interactive"]:
+      outputPipe = None
+    else:
+      outputPipe = subprocess.PIPE
+
+    proc = self.Process([cmd] + args,
+                 env = self.environment(env, xrePath = xrePath,
                                    crashreporter = not debuggerInfo),
                  stdout = outputPipe,
                  stderr = subprocess.STDOUT)
-  log.info("INFO | automation.py | Application pid: %d", proc.pid)
+    self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
 
-  stackFixerProcess = None
-  didTimeout = False
-  if outputPipe is None:
-    log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
-  else:
-    logsource = proc.stdout
-    if IS_DEBUG_BUILD:
-      stackFixerCommand = None
-      if IS_MAC:
-        stackFixerCommand = "fix-macosx-stack.pl"
-      elif IS_LINUX:
-        stackFixerCommand = "fix-linux-stack.pl"
-      if stackFixerCommand is not None:
-        stackFixerProcess = Process([PERL, os.path.join(utilityPath, stackFixerCommand)], stdin=logsource, stdout=subprocess.PIPE)
-        logsource = stackFixerProcess.stdout
+    stackFixerProcess = None
+    didTimeout = False
+    if outputPipe is None:
+      self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
+    else:
+      logsource = proc.stdout
+      if self.IS_DEBUG_BUILD:
+        stackFixerCommand = None
+        if self.IS_MAC:
+          stackFixerCommand = "fix-macosx-stack.pl"
+        elif self.IS_LINUX:
+          stackFixerCommand = "fix-linux-stack.pl"
+        if stackFixerCommand is not None:
+          stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, stackFixerCommand)], 
+                                           stdin=logsource, 
+                                           stdout=subprocess.PIPE)
+          logsource = stackFixerProcess.stdout
 
-    (line, didTimeout) = readWithTimeout(logsource, timeout)
-    hitMaxTime = False
-    while line != "" and not didTimeout:
-      log.info(line.rstrip())
-      (line, didTimeout) = readWithTimeout(logsource, timeout)
-      if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
-        # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
-        hitMaxTime = True
-        log.info("TEST-UNEXPECTED-FAIL | automation.py | application ran for longer than allowed maximum time of %d seconds", int(maxTime))
-        triggerBreakpad(proc, utilityPath)
-    if didTimeout:
-      log.info("TEST-UNEXPECTED-FAIL | automation.py | application timed out after %d seconds with no output", int(timeout))
-      triggerBreakpad(proc, utilityPath)
+      (line, didTimeout) = self.readWithTimeout(logsource, timeout)
+      hitMaxTime = False
+      while line != "" and not didTimeout:
+        self.log.info(line.rstrip())
+        (line, didTimeout) = self.readWithTimeout(logsource, timeout)
+        if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
+          # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
+          hitMaxTime = True
+          self.log.info("TEST-UNEXPECTED-FAIL | automation.py | application ran for longer than allowed maximum time of %d seconds", int(maxTime))
+          self.triggerBreakpad(proc, utilityPath)
+      if didTimeout:
+        self.log.info("TEST-UNEXPECTED-FAIL | automation.py | application timed out after %d seconds with no output", int(timeout))
+        self.triggerBreakpad(proc, utilityPath)
 
-  status = proc.wait()
-  if status != 0 and not didTimeout and not hitMaxTime:
-    log.info("TEST-UNEXPECTED-FAIL | automation.py | Exited with code %d during test run", status)
-  if stackFixerProcess is not None:
-    fixerStatus = stackFixerProcess.wait()
-    if fixerStatus != 0 and not didTimeout and not hitMaxTime:
-      log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
-  log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
+    status = proc.wait()
+    if status != 0 and not didTimeout and not hitMaxTime:
+      self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Exited with code %d during test run", status)
+    if stackFixerProcess is not None:
+      fixerStatus = stackFixerProcess.wait()
+      if fixerStatus != 0 and not didTimeout and not hitMaxTime:
+        self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
+    self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
 
-  # Do a final check for zombie child processes.
-  if not os.path.exists(processLog):
-    log.info('INFO | automation.py | PID log not found: %s', processLog)
-  else:
-    log.info('INFO | automation.py | Reading PID log: %s', processLog)
-    processList = []
-    pidRE = re.compile(r'launched child process (\d+)$')
-    processLogFD = open(processLog)
-    for line in processLogFD:
-      log.info(line.rstrip())
-      m = pidRE.search(line)
-      if m:
-        processList.append(int(m.group(1)))
-    processLogFD.close()
+    # Do a final check for zombie child processes.
+    if not os.path.exists(processLog):
+      self.log.info('INFO | automation.py | PID log not found: %s', processLog)
+    else:
+      self.log.info('INFO | automation.py | Reading PID log: %s', processLog)
+      processList = []
+      pidRE = re.compile(r'launched child process (\d+)$')
+      processLogFD = open(processLog)
+      for line in processLogFD:
+        self.log.info(line.rstrip())
+        m = pidRE.search(line)
+        if m:
+          processList.append(int(m.group(1)))
+      processLogFD.close()
 
-    for processPID in processList:
-      log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID)
-      if isPidAlive(processPID):
-        log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
-        killPid(processPID)
+      for processPID in processList:
+        self.log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID)
+        if self.isPidAlive(processPID):
+          self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
+          self.killPid(processPID)
 
-  if checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath):
-    status = -1
+    if self.automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath):
+      status = -1
 
-  if os.path.exists(processLog):
-    os.unlink(processLog)
+    if os.path.exists(processLog):
+      os.unlink(processLog)
 
-  if IS_TEST_BUILD and runSSLTunnel:
-    ssltunnelProcess.kill()
+    if self.IS_TEST_BUILD and runSSLTunnel:
+      ssltunnelProcess.kill()
 
-  return status
+    return status
--- a/build/leaktest.py.in
+++ b/build/leaktest.py.in
@@ -41,28 +41,29 @@
 
 import SimpleHTTPServer
 import SocketServer
 import threading
 import os
 import sys
 import logging
 from getopt import getopt
-import automation
+from automation import Automation
 
 PORT = 8888
 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
 PROFILE_DIRECTORY = os.path.abspath(os.path.join(SCRIPT_DIR, "./leakprofile"))
-DIST_BIN = os.path.join(SCRIPT_DIR, automation.DIST_BIN)
 os.chdir(SCRIPT_DIR)
 
 class EasyServer(SocketServer.TCPServer):
     allow_reuse_address = True
 
 if __name__ == '__main__':
+    automation = Automation()
+    DIST_BIN = os.path.join(SCRIPT_DIR, automation.DIST_BIN)
     opts, extraArgs = getopt(sys.argv[1:], 'l:')
     if len(opts) > 0:
         try:
             automation.log.addHandler(logging.FileHandler(opts[0][1], "w"))
         except:
             automation.log.info("Unable to open logfile " + opts[0][1] + \
                                 "ONLY logging to stdout.")
 
--- a/build/pgo/genpgocert.py.in
+++ b/build/pgo/genpgocert.py.in
@@ -31,27 +31,29 @@
 # 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 automation
+from automation import Automation
 import os
 import re
 import shutil
 import sys
 
 #expand DIST_BIN = __XPC_BIN_PATH__
 #expand BIN_SUFFIX = __BIN_SUFFIX__
 #expand PROFILE_DIR = __PROFILE_DIR__
 #expand CERTS_SRC_DIR = __CERTS_SRC_DIR__
 
+automation = Automation()
+
 dbFiles = [
   re.compile("^cert[0-9]+\.db$"),
   re.compile("^key[0-9]+\.db$"),
   re.compile("^secmod\.db$")
 ]
 
 def unlinkDbFiles(path):
   for root, dirs, files in os.walk(path):
--- a/build/pgo/profileserver.py.in
+++ b/build/pgo/profileserver.py.in
@@ -41,27 +41,28 @@
 import SimpleHTTPServer
 import SocketServer
 import socket
 import threading
 import os
 import sys
 import shutil
 from datetime import datetime
-import automation
+from automation import Automation
 
 PORT = 8888
 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
 PROFILE_DIRECTORY = os.path.abspath(os.path.join(SCRIPT_DIR, "./pgoprofile"))
 os.chdir(SCRIPT_DIR)
 
 class EasyServer(SocketServer.TCPServer):
   allow_reuse_address = True
 
 if __name__ == '__main__':
+  automation = Automation()
   httpd = EasyServer(("", PORT), SimpleHTTPServer.SimpleHTTPRequestHandler)
   t = threading.Thread(target=httpd.serve_forever)
   t.setDaemon(True) # don't hang on exit
   t.start()
 
   automation.initializeProfile(PROFILE_DIRECTORY)
   browserEnv = automation.environment()
   browserEnv["XPCOM_DEBUG_BREAK"] = "warn"
--- a/layout/tools/reftest/runreftest.py
+++ b/layout/tools/reftest/runreftest.py
@@ -39,64 +39,138 @@
 
 """
 Runs the reftest test harness.
 """
 
 import sys, shutil, os, os.path
 SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
 sys.path.append(SCRIPT_DIRECTORY)
-import automation
+from automation import Automation
 from automationutils import *
 from optparse import OptionParser
 from tempfile import mkdtemp
 
-oldcwd = os.getcwd()
-os.chdir(SCRIPT_DIRECTORY)
+class RefTest(object):
+
+  oldcwd = os.getcwd()
+
+  def __init__(self, automation):
+    self.automation = automation
+    os.chdir(SCRIPT_DIRECTORY)
 
-def getFullPath(path):
-  "Get an absolute path relative to oldcwd."
-  return os.path.normpath(os.path.join(oldcwd, os.path.expanduser(path)))
+  def getFullPath(self, path):
+    "Get an absolute path relative to self.oldcwd."
+    return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
+
+  def createReftestProfile(self, options, profileDir):
+    "Sets up a profile for reftest."
+
+    # Set preferences.
+    prefsFile = open(os.path.join(profileDir, "user.js"), "w")
+    prefsFile.write("""user_pref("browser.dom.window.dump.enabled", true);
+    """)
+    prefsFile.write('user_pref("reftest.timeout", %d);\n' % (options.timeout * 1000))
+    prefsFile.write('user_pref("ui.caretBlinkTime", -1);\n')
 
-def createReftestProfile(options, profileDir):
-  "Sets up a profile for reftest."
+    for v in options.extraPrefs:
+      thispref = v.split("=")
+      if len(thispref) < 2:
+        print "Error: syntax error in --setpref=" + v
+        sys.exit(1)
+      part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
+      prefsFile.write(part)
+    # no slow script dialogs
+    prefsFile.write('user_pref("dom.max_script_run_time", 0);')
+    prefsFile.write('user_pref("dom.max_chrome_script_run_time", 0);')
+    prefsFile.close()
 
-  # Set preferences.
-  prefsFile = open(os.path.join(profileDir, "user.js"), "w")
-  prefsFile.write("""user_pref("browser.dom.window.dump.enabled", true);
-""")
-  prefsFile.write('user_pref("reftest.timeout", %d);\n' % (options.timeout * 1000))
-  prefsFile.write('user_pref("ui.caretBlinkTime", -1);\n')
+    # install the reftest extension bits into the profile
+    profileExtensionsPath = os.path.join(profileDir, "extensions")
+    os.mkdir(profileExtensionsPath)
+    reftestExtensionPath = os.path.join(SCRIPT_DIRECTORY, "reftest")
+    extFile = open(os.path.join(profileExtensionsPath, "reftest@mozilla.org"), "w")
+    extFile.write(reftestExtensionPath)
+    extFile.close()
+
+  def runTests(self, manifest, options):
+    debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs,
+        options.debuggerInteractive);
+
+    profileDir = None
+    try:
+      profileDir = mkdtemp()
+      self.createReftestProfile(options, profileDir)
+      self.copyExtraFilesToProfile(options, profileDir)
 
-  for v in options.extraPrefs:
-    thispref = v.split("=")
-    if len(thispref) < 2:
-      print "Error: syntax error in --setpref=" + v
-      sys.exit(1)
-    part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
-    prefsFile.write(part)
-  # no slow script dialogs
-  prefsFile.write('user_pref("dom.max_script_run_time", 0);')
-  prefsFile.write('user_pref("dom.max_chrome_script_run_time", 0);')
-  prefsFile.close()
+      # browser environment
+      browserEnv = self.automation.environment(xrePath = options.xrePath)
+      browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
+
+      # Enable leaks detection to its own log file.
+      leakLogFile = os.path.join(profileDir, "runreftest_leaks.log")
+      browserEnv["XPCOM_MEM_BLOAT_LOG"] = leakLogFile
+
+      # run once with -silent to let the extension manager do its thing
+      # and then exit the app
+      self.automation.log.info("REFTEST INFO | runreftest.py | Performing extension manager registration: start.\n")
+      # Don't care about this |status|: |runApp()| reporting it should be enough.
+      status = self.automation.runApp(None, browserEnv, options.app, profileDir,
+                                 ["-silent"],
+                                 utilityPath = options.utilityPath,
+                                 xrePath=options.xrePath,
+                                 symbolsPath=options.symbolsPath)
+      # We don't care to call |processLeakLog()| for this step.
+      self.automation.log.info("\nREFTEST INFO | runreftest.py | Performing extension manager registration: end.")
+
+      # Remove the leak detection file so it can't "leak" to the tests run.
+      # The file is not there if leak logging was not enabled in the application build.
+      if os.path.exists(leakLogFile):
+        os.remove(leakLogFile)
 
-  # install the reftest extension bits into the profile
-  profileExtensionsPath = os.path.join(profileDir, "extensions")
-  os.mkdir(profileExtensionsPath)
-  reftestExtensionPath = os.path.join(SCRIPT_DIRECTORY, "reftest")
-  extFile = open(os.path.join(profileExtensionsPath, "reftest@mozilla.org"), "w")
-  extFile.write(reftestExtensionPath)
-  extFile.close()
+      # then again to actually run reftest
+      self.automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n")
+      reftestlist = self.getFullPath(manifest)
+      status = self.automation.runApp(None, browserEnv, options.app, profileDir,
+                                 ["-reftest", reftestlist],
+                                 utilityPath = options.utilityPath,
+                                 xrePath=options.xrePath,
+                                 debuggerInfo=debuggerInfo,
+                                 symbolsPath=options.symbolsPath,
+                                 # give the JS harness 30 seconds to deal
+                                 # with its own timeouts
+                                 timeout=options.timeout + 30.0)
+      processLeakLog(leakLogFile, options.leakThreshold)
+      self.automation.log.info("\nREFTEST INFO | runreftest.py | Running tests: end.")
+    finally:
+      if profileDir:
+        shutil.rmtree(profileDir)
+    return status
+
+  def copyExtraFilesToProfile(self, options, profileDir):
+    "Copy extra files or dirs specified on the command line to the testing profile."
+    for f in options.extraProfileFiles:
+      abspath = self.getFullPath(f)
+      dest = os.path.join(profileDir, os.path.basename(abspath))
+      if os.path.isdir(abspath):
+        shutil.copytree(abspath, dest)
+      else:
+        shutil.copy(abspath, dest)
+
 
 def main():
+  automation = Automation()
   parser = OptionParser()
+  reftest = RefTest(automation)
 
   # we want to pass down everything from automation.__all__
-  addCommonOptions(parser, defaults=dict(zip(automation.__all__, [getattr(automation, x) for x in automation.__all__])))
-  automation.addExtraCommonOptions(parser)
+  addCommonOptions(parser, 
+                   defaults=dict(zip(automation.__all__, 
+                            [getattr(automation, x) for x in automation.__all__])))
+  automation.addCommonOptions(parser)
   parser.add_option("--appname",
                     action = "store", type = "string", dest = "app",
                     default = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP),
                     help = "absolute path to application, overriding default")
   parser.add_option("--extra-profile-file",
                     action = "append", dest = "extraProfileFiles",
                     default = [],
                     help = "copy specified files/dirs to testing profile")
@@ -113,95 +187,33 @@ def main():
                            "than the given number")
   parser.add_option("--utility-path",
                     action = "store", type = "string", dest = "utilityPath",
                     default = automation.DIST_BIN,
                     help = "absolute path to directory containing utility "
                            "programs (xpcshell, ssltunnel, certutil)")
 
   options, args = parser.parse_args()
-
   if len(args) != 1:
     print >>sys.stderr, "No reftest.list specified."
     sys.exit(1)
 
-  options.app = getFullPath(options.app)
+  options.app = reftest.getFullPath(options.app)
   if not os.path.exists(options.app):
     print """Error: Path %(app)s doesn't exist.
 Are you executing $objdir/_tests/reftest/runreftest.py?""" \
-        % {"app": options.app}
+            % {"app": options.app}
     sys.exit(1)
 
   if options.xrePath is None:
     options.xrePath = os.path.dirname(options.app)
   else:
     # allow relative paths
-    options.xrePath = getFullPath(options.xrePath)
+    options.xrePath = reftest.getFullPath(options.xrePath)
 
   if options.symbolsPath:
-    options.symbolsPath = getFullPath(options.symbolsPath)
-  options.utilityPath = getFullPath(options.utilityPath)
-
-  debuggerInfo = getDebuggerInfo(oldcwd, options.debugger, options.debuggerArgs,
-     options.debuggerInteractive);
-
-  profileDir = None
-  try:
-    profileDir = mkdtemp()
-    createReftestProfile(options, profileDir)
-    copyExtraFilesToProfile(options, profileDir)
-
-    # browser environment
-    browserEnv = automation.environment(xrePath = options.xrePath)
-    browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
-
-    # Enable leaks detection to its own log file.
-    leakLogFile = os.path.join(profileDir, "runreftest_leaks.log")
-    browserEnv["XPCOM_MEM_BLOAT_LOG"] = leakLogFile
-
-    # run once with -silent to let the extension manager do its thing
-    # and then exit the app
-    automation.log.info("REFTEST INFO | runreftest.py | Performing extension manager registration: start.\n")
-    # Don't care about this |status|: |runApp()| reporting it should be enough.
-    status = automation.runApp(None, browserEnv, options.app, profileDir,
-                               ["-silent"],
-                               utilityPath = options.utilityPath,
-                               xrePath=options.xrePath,
-                               symbolsPath=options.symbolsPath)
-    # We don't care to call |processLeakLog()| for this step.
-    automation.log.info("\nREFTEST INFO | runreftest.py | Performing extension manager registration: end.")
+    options.symbolsPath = reftest.getFullPath(options.symbolsPath)
+  options.utilityPath = reftest.getFullPath(options.utilityPath)
 
-    # Remove the leak detection file so it can't "leak" to the tests run.
-    # The file is not there if leak logging was not enabled in the application build.
-    if os.path.exists(leakLogFile):
-      os.remove(leakLogFile)
-
-    # then again to actually run reftest
-    automation.log.info("REFTEST INFO | runreftest.py | Running tests: start.\n")
-    reftestlist = getFullPath(args[0])
-    status = automation.runApp(None, browserEnv, options.app, profileDir,
-                               ["-reftest", reftestlist],
-                               utilityPath = options.utilityPath,
-                               xrePath=options.xrePath,
-                               debuggerInfo=debuggerInfo,
-                               symbolsPath=options.symbolsPath,
-                               # give the JS harness 30 seconds to deal
-                               # with its own timeouts
-                               timeout=options.timeout + 30.0)
-    processLeakLog(leakLogFile, options.leakThreshold)
-    automation.log.info("\nREFTEST INFO | runreftest.py | Running tests: end.")
-  finally:
-    if profileDir:
-      shutil.rmtree(profileDir)
-  sys.exit(status)
-
-def copyExtraFilesToProfile(options, profileDir):
-  "Copy extra files or dirs specified on the command line to the testing profile."
-  for f in options.extraProfileFiles:
-    abspath = getFullPath(f)
-    dest = os.path.join(profileDir, os.path.basename(abspath))
-    if os.path.isdir(abspath):
-      shutil.copytree(abspath, dest)
-    else:
-      shutil.copy(abspath, dest)
-
+  sys.exit(reftest.runTests(args[0], options))
+  
 if __name__ == "__main__":
   main()
--- a/testing/mochitest/runtests.py.in
+++ b/testing/mochitest/runtests.py.in
@@ -47,84 +47,55 @@ import optparse
 import os
 import os.path
 import sys
 import time
 import shutil
 from urllib import quote_plus as encodeURIComponent
 import urllib2
 import commands
-import automation
+from automation import Automation
 from automationutils import *
 
-# Path to the test script on the server
-TEST_SERVER_HOST = "localhost:8888"
-TEST_PATH = "/tests/"
-CHROME_PATH = "/redirect.html";
-A11Y_PATH = "/redirect-a11y.html"
-TESTS_URL = "http://" + TEST_SERVER_HOST + TEST_PATH
-CHROMETESTS_URL = "http://" + TEST_SERVER_HOST + CHROME_PATH
-A11YTESTS_URL = "http://" + TEST_SERVER_HOST + A11Y_PATH
-SERVER_SHUTDOWN_URL = "http://" + TEST_SERVER_HOST + "/server/shutdown"
-# main browser chrome URL, same as browser.chromeURL pref
-#ifdef MOZ_SUITE
-BROWSER_CHROME_URL = "chrome://navigator/content/navigator.xul"
-#else
-BROWSER_CHROME_URL = "chrome://browser/content/browser.xul"
-#endif
-
-# Max time in seconds to wait for server startup before tests will fail -- if
-# this seems big, it's mostly for debug machines where cold startup
-# (particularly after a build) takes forever.
-if automation.IS_DEBUG_BUILD:
-    SERVER_STARTUP_TIMEOUT = 180
-else:
-    SERVER_STARTUP_TIMEOUT = 90
-
-oldcwd = os.getcwd()
-SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
-os.chdir(SCRIPT_DIRECTORY)
-
-PROFILE_DIRECTORY = os.path.abspath("./mochitesttestingprofile")
-
-LEAK_REPORT_FILE = os.path.join(PROFILE_DIRECTORY, "runtests_leaks.log")
 
 #######################
 # COMMANDLINE OPTIONS #
 #######################
 
 class MochitestOptions(optparse.OptionParser):
   """Parses Mochitest commandline options."""
-  def __init__(self, **kwargs):
+  def __init__(self, automation, scriptdir, **kwargs):
+    self._automation = automation
     optparse.OptionParser.__init__(self, **kwargs)
     defaults = {}
 
-    # we want to pass down everything from automation.__all__
-    addCommonOptions(self, defaults=dict(zip(automation.__all__, [getattr(automation, x) for x in automation.__all__])))
-    automation.addExtraCommonOptions(self)
+    # we want to pass down everything from self._automation.__all__
+    addCommonOptions(self, defaults=dict(zip(self._automation.__all__, 
+             [getattr(self._automation, x) for x in self._automation.__all__])))
+    self._automation.addCommonOptions(self)
 
     self.add_option("--close-when-done",
                     action = "store_true", dest = "closeWhenDone",
                     help = "close the application when tests are done running")
     defaults["closeWhenDone"] = False
 
     self.add_option("--appname",
                     action = "store", type = "string", dest = "app",
                     help = "absolute path to application, overriding default")
-    defaults["app"] = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP)
+    defaults["app"] = os.path.join(scriptdir, self._automation.DEFAULT_APP)
 
     self.add_option("--utility-path",
                     action = "store", type = "string", dest = "utilityPath",
                     help = "absolute path to directory containing utility programs (xpcshell, ssltunnel, certutil)")
-    defaults["utilityPath"] = automation.DIST_BIN
+    defaults["utilityPath"] = self._automation.DIST_BIN
 
     self.add_option("--certificate-path",
                     action = "store", type = "string", dest = "certPath",
                     help = "absolute path to directory containing certificate store to use testing profile")
-    defaults["certPath"] = automation.CERTS_SRC_DIR
+    defaults["certPath"] = self._automation.CERTS_SRC_DIR
 
     self.add_option("--log-file",
                     action = "store", type = "string",
                     dest = "logFile", metavar = "FILE",
                     help = "file to which logging occurs")
     defaults["logFile"] = ""
 
     self.add_option("--autorun",
@@ -244,48 +215,49 @@ See <http://mochikit.com/doc/html/MochiK
 
 #######################
 # HTTP SERVER SUPPORT #
 #######################
 
 class MochitestServer:
   "Web server used to serve Mochitests, for closer fidelity to the real web."
 
-  def __init__(self, options):
+  def __init__(self, automation, options, profileDir):
+    self._automation = automation
     self._closeWhenDone = options.closeWhenDone
     self._utilityPath = options.utilityPath
     self._xrePath = options.xrePath
+    self._profileDir = profileDir
 
   def start(self):
     "Run the Mochitest server, returning the process ID of the server."
     
-    env = automation.environment(xrePath = self._xrePath)
+    env = self._automation.environment(xrePath = self._xrePath)
     env["XPCOM_DEBUG_BREAK"] = "warn"
-    if automation.IS_WIN32:
+    if self._automation.IS_WIN32:
       env["PATH"] = env["PATH"] + ";" + self._xrePath
 
     args = ["-g", self._xrePath,
             "-v", "170",
             "-f", "./" + "httpd.js",
             "-f", "./" + "server.js"]
 
     xpcshell = os.path.join(self._utilityPath,
-                            "xpcshell" + automation.BIN_SUFFIX)
-    self._process = automation.Process([xpcshell] + args, env = env)
+                            "xpcshell" + self._automation.BIN_SUFFIX)
+    self._process = self._automation.Process([xpcshell] + args, env = env)
     pid = self._process.pid
     if pid < 0:
       print "Error starting server."
       sys.exit(2)
-    automation.log.info("INFO | runtests.py | Server pid: %d", pid)
-    
+    self._automation.log.info("INFO | runtests.py | Server pid: %d", pid)
 
   def ensureReady(self, timeout):
     assert timeout >= 0
 
-    aliveFile = os.path.join(PROFILE_DIRECTORY, "server_alive.txt")
+    aliveFile = os.path.join(self._profileDir, "server_alive.txt")
     i = 0
     while i < timeout:
       if os.path.exists(aliveFile):
         break
       time.sleep(1)
       i += 1
     else:
       print "Timed out while waiting for server startup."
@@ -296,282 +268,301 @@ class MochitestServer:
     try:
       c = urllib2.urlopen(SERVER_SHUTDOWN_URL)
       c.read()
       c.close()
       self._process.wait()
     except:
       self._process.kill()
 
-def getFullPath(path):
-  "Get an absolute path relative to oldcwd."
-  return os.path.normpath(os.path.join(oldcwd, os.path.expanduser(path)))
+
+class Mochitest(object):
+  # Path to the test script on the server
+  TEST_SERVER_HOST = "localhost:8888"
+  TEST_PATH = "/tests/"
+  CHROME_PATH = "/redirect.html";
+  A11Y_PATH = "/redirect-a11y.html"
+  TESTS_URL = "http://" + TEST_SERVER_HOST + TEST_PATH
+  CHROMETESTS_URL = "http://" + TEST_SERVER_HOST + CHROME_PATH
+  A11YTESTS_URL = "http://" + TEST_SERVER_HOST + A11Y_PATH
+  SERVER_SHUTDOWN_URL = "http://" + TEST_SERVER_HOST + "/server/shutdown"
+ 
+  oldcwd = os.getcwd()
+
+  def __init__(self, automation):
+    self.automation = automation
+
+    # Max time in seconds to wait for server startup before tests will fail -- if
+    # this seems big, it's mostly for debug machines where cold startup
+    # (particularly after a build) takes forever.
+    if self.automation.IS_DEBUG_BUILD:
+      self.SERVER_STARTUP_TIMEOUT = 180
+    else:
+      self.SERVER_STARTUP_TIMEOUT = 90
+
+    self.SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
+    os.chdir(self.SCRIPT_DIRECTORY)
+
+    self.PROFILE_DIRECTORY = os.path.abspath("./mochitesttestingprofile")
+
+    self.LEAK_REPORT_FILE = os.path.join(self.PROFILE_DIRECTORY, "runtests_leaks.log")
+
+  def getFullPath(self, path):
+    "Get an absolute path relative to self.oldcwd."
+    return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
+
+  def runTests(self, options):
+    debuggerInfo = getDebuggerInfo(self.oldcwd, options.debugger, options.debuggerArgs,
+                      options.debuggerInteractive);
+
+    # browser environment
+    browserEnv = self.automation.environment(xrePath = options.xrePath)
+
+    # These variables are necessary for correct application startup; change
+    # via the commandline at your own risk.
+    browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
+
+    for v in options.environment:
+      ix = v.find("=")
+      if ix <= 0:
+        print "Error: syntax error in --setenv=" + v
+        return 1
+      browserEnv[v[:ix]] = v[ix + 1:]
+
+    self.automation.initializeProfile(self.PROFILE_DIRECTORY, options.extraPrefs)
+    manifest = self.addChromeToProfile(options)
+    self.copyExtraFilesToProfile(options)
+    server = MochitestServer(self.automation, options, self.PROFILE_DIRECTORY)
+    server.start()
+
+    # If we're lucky, the server has fully started by now, and all paths are
+    # ready, etc.  However, xpcshell cold start times suck, at least for debug
+    # builds.  We'll try to connect to the server for awhile, and if we fail,
+    # we'll try to kill the server and exit with an error.
+    server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
+
+    # URL parameters to test URL:
+    #
+    # autorun -- kick off tests automatically
+    # closeWhenDone -- runs quit.js after tests
+    # logFile -- logs test run to an absolute path
+    # totalChunks -- how many chunks to split tests into
+    # thisChunk -- which chunk to run
+    # timeout -- per-test timeout in seconds
+    #
+  
+    # consoleLevel, fileLevel: set the logging level of the console and
+    # file logs, if activated.
+    # <http://mochikit.com/doc/html/MochiKit/Logging.html>
+
+    testURL = self.TESTS_URL + options.testPath
+    urlOpts = []
+    if options.chrome:
+      testURL = self.CHROMETESTS_URL
+      if options.testPath:
+        urlOpts.append("testPath=" + encodeURIComponent(options.testPath))
+    elif options.a11y:
+      testURL = self.A11YTESTS_URL
+      if options.testPath:
+        urlOpts.append("testPath=" + encodeURIComponent(options.testPath))
+    elif options.browserChrome:
+      testURL = "about:blank"
+
+    # allow relative paths for logFile
+    if options.logFile:
+      options.logFile = self.getFullPath(options.logFile)
+    if options.browserChrome:
+      self.makeTestConfig(options)
+    else:
+      if options.autorun:
+        urlOpts.append("autorun=1")
+      if options.timeout:
+        urlOpts.append("timeout=%d" % options.timeout)
+      if options.closeWhenDone:
+        urlOpts.append("closeWhenDone=1")
+      if options.logFile:
+        urlOpts.append("logFile=" + encodeURIComponent(options.logFile))
+        urlOpts.append("fileLevel=" + encodeURIComponent(options.fileLevel))
+      if options.consoleLevel:
+        urlOpts.append("consoleLevel=" + encodeURIComponent(options.consoleLevel))
+      if options.totalChunks:
+        urlOpts.append("totalChunks=%d" % options.totalChunks)
+        urlOpts.append("thisChunk=%d" % options.thisChunk)
+      if options.chunkByDir:
+        urlOpts.append("chunkByDir=%d" % options.chunkByDir)
+      if options.shuffle:
+        urlOpts.append("shuffle=1")
+      if len(urlOpts) > 0:
+        testURL += "?" + "&".join(urlOpts)
+
+    browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.LEAK_REPORT_FILE
+
+    if options.fatalAssertions:
+      browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
 
-#################
-# MAIN FUNCTION #
-#################
+    # run once with -silent to let the extension manager do its thing
+    # and then exit the app
+    self.automation.log.info("INFO | runtests.py | Performing extension manager registration: start.\n")
+    # Don't care about this |status|: |runApp()| reporting it should be enough.
+    status = self.automation.runApp(None, browserEnv, options.app,
+                                self.PROFILE_DIRECTORY, ["-silent"],
+                                utilityPath = options.utilityPath,
+                                xrePath = options.xrePath,
+                                symbolsPath=options.symbolsPath)
+    # We don't care to call |processLeakLog()| for this step.
+    self.automation.log.info("\nINFO | runtests.py | Performing extension manager registration: end.")
+
+    # Remove the leak detection file so it can't "leak" to the tests run.
+    # The file is not there if leak logging was not enabled in the application build.
+    if os.path.exists(self.LEAK_REPORT_FILE):
+      os.remove(self.LEAK_REPORT_FILE)
+
+    # then again to actually run mochitest
+    if options.timeout:
+      timeout = options.timeout + 30
+    elif options.autorun:
+      timeout = None
+    else:
+      timeout = 330.0 # default JS harness timeout is 300 seconds
+    self.automation.log.info("INFO | runtests.py | Running tests: start.\n")
+    status = self.automation.runApp(testURL, browserEnv, options.app,
+                                self.PROFILE_DIRECTORY, options.browserArgs,
+                                runSSLTunnel = True,
+                                utilityPath = options.utilityPath,
+                                xrePath = options.xrePath,
+                                certPath=options.certPath,
+                                debuggerInfo=debuggerInfo,
+                                symbolsPath=options.symbolsPath,
+                                timeout = timeout)
+
+    # Server's no longer needed, and perhaps more importantly, anything it might
+    # spew to console shouldn't disrupt the leak information table we print next.
+    server.stop()
+
+    processLeakLog(self.LEAK_REPORT_FILE, options.leakThreshold)
+    self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
+
+    # delete the profile and manifest
+    os.remove(manifest)
+
+    # hanging due to non-halting threads is no fun; assume we hit the errors we
+    # were going to hit already and exit.
+    return status
+
+  def makeTestConfig(self, options):
+    "Creates a test configuration file for customizing test execution."
+    def boolString(b):
+      if b:
+        return "true"
+      return "false"
+
+    logFile = options.logFile.replace("\\", "\\\\")
+    testPath = options.testPath.replace("\\", "\\\\")
+    content = """\
+({
+  autoRun: %(autorun)s,
+  closeWhenDone: %(closeWhenDone)s,
+  logPath: "%(logPath)s",
+  testPath: "%(testPath)s"
+})""" % {"autorun": boolString(options.autorun),
+         "closeWhenDone": boolString(options.closeWhenDone),
+         "logPath": logFile,
+         "testPath": testPath}
+
+    config = open(os.path.join(self.PROFILE_DIRECTORY, "testConfig.js"), "w")
+    config.write(content)
+    config.close() 
+
+
+  def addChromeToProfile(self, options):
+    "Adds MochiKit chrome tests to the profile."
+
+    chromedir = os.path.join(self.PROFILE_DIRECTORY, "chrome")
+    os.mkdir(chromedir)
+
+    chrome = """
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */
+toolbar,
+toolbarpalette {
+  background-color: rgb(235, 235, 235) !important;
+}
+toolbar#nav-bar {
+  background-image: none !important;
+}
+"""
+
+    # write userChrome.css
+    chromeFile = open(os.path.join(self.PROFILE_DIRECTORY, "userChrome.css"), "a")
+    chromeFile.write(chrome)
+    chromeFile.close()
+
+
+    # register our chrome dir
+    chrometestDir = os.path.abspath(".") + "/"
+    if self.automation.IS_WIN32:
+      chrometestDir = "file:///" + chrometestDir.replace("\\", "/")
+
+
+    (path, leaf) = os.path.split(options.app)
+    manifest = os.path.join(path, "chrome", "mochikit.manifest")
+    manifestFile = open(manifest, "w")
+    manifestFile.write("content mochikit " + chrometestDir + " contentaccessible=yes\n")
+
+    if options.browserChrome:
+      manifestFile.write("""overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul
+overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul
+""")
+    manifestFile.close()
+
+    return manifest
+
+  def copyExtraFilesToProfile(self, options):
+    "Copy extra files or dirs specified on the command line to the testing profile."
+    for f in options.extraProfileFiles:
+      abspath = self.getFullPath(f)
+      dest = os.path.join(self.PROFILE_DIRECTORY, os.path.basename(abspath))
+      if os.path.isdir(abspath):
+        shutil.copytree(abspath, dest)
+      else:
+        shutil.copy(abspath, dest)
 
 def main():
-  parser = MochitestOptions()
+  automation = Automation()
+  mochitest = Mochitest(automation)
+  parser = MochitestOptions(automation, mochitest.SCRIPT_DIRECTORY)
   options, args = parser.parse_args()
 
   if options.totalChunks is not None and options.thisChunk is None:
     parser.error("thisChunk must be specified when totalChunks is specified")
 
   if options.totalChunks:
     if not 1 <= options.thisChunk <= options.totalChunks:
-       parser.error("thisChunk must be between 1 and totalChunks")
+      parser.error("thisChunk must be between 1 and totalChunks")
 
   if options.xrePath is None:
     # default xrePath to the app path if not provided
     # but only if an app path was explicitly provided
     if options.app != parser.defaults['app']:
       options.xrePath = os.path.dirname(options.app)
     else:
       # otherwise default to dist/bin
       options.xrePath = automation.DIST_BIN
 
   # allow relative paths
-  options.xrePath = getFullPath(options.xrePath)
+  options.xrePath = mochitest.getFullPath(options.xrePath)
 
-  options.app = getFullPath(options.app)
+  options.app = mochitest.getFullPath(options.app)
   if not os.path.exists(options.app):
     msg = """\
-Error: Path %(app)s doesn't exist.
-Are you executing $objdir/_tests/testing/mochitest/runtests.py?"""
+    Error: Path %(app)s doesn't exist.
+    Are you executing $objdir/_tests/testing/mochitest/runtests.py?"""
     print msg % {"app": options.app}
     sys.exit(1)
 
-  options.utilityPath = getFullPath(options.utilityPath)
-  options.certPath = getFullPath(options.certPath)
+  options.utilityPath = mochitest.getFullPath(options.utilityPath)
+  options.certPath = mochitest.getFullPath(options.certPath)
   if options.symbolsPath:
-    options.symbolsPath = getFullPath(options.symbolsPath)
-
-  debuggerInfo = getDebuggerInfo(oldcwd, options.debugger, options.debuggerArgs,
-    options.debuggerInteractive);
-
-  # browser environment
-  browserEnv = automation.environment(xrePath = options.xrePath)
-
-  # These variables are necessary for correct application startup; change
-  # via the commandline at your own risk.
-  browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
-
-  for v in options.environment:
-    ix = v.find("=")
-    if ix <= 0:
-      print "Error: syntax error in --setenv=" + v
-      sys.exit(1)
-    browserEnv[v[:ix]] = v[ix + 1:]
-
-  automation.initializeProfile(PROFILE_DIRECTORY, options.extraPrefs)
-  manifest = addChromeToProfile(options)
-  copyExtraFilesToProfile(options)
-  server = MochitestServer(options)
-  server.start()
-
-  # If we're lucky, the server has fully started by now, and all paths are
-  # ready, etc.  However, xpcshell cold start times suck, at least for debug
-  # builds.  We'll try to connect to the server for awhile, and if we fail,
-  # we'll try to kill the server and exit with an error.
-  server.ensureReady(SERVER_STARTUP_TIMEOUT)
-
-  # URL parameters to test URL:
-  #
-  # autorun -- kick off tests automatically
-  # closeWhenDone -- runs quit.js after tests
-  # logFile -- logs test run to an absolute path
-  # totalChunks -- how many chunks to split tests into
-  # thisChunk -- which chunk to run
-  # timeout -- per-test timeout in seconds
-  #
-  
-  # consoleLevel, fileLevel: set the logging level of the console and
-  # file logs, if activated.
-  # <http://mochikit.com/doc/html/MochiKit/Logging.html>
-
-  testURL = TESTS_URL + options.testPath
-  urlOpts = []
-  if options.chrome:
-    testURL = CHROMETESTS_URL
-    if options.testPath:
-      urlOpts.append("testPath=" + encodeURIComponent(options.testPath))
-  elif options.a11y:
-    testURL = A11YTESTS_URL
-    if options.testPath:
-      urlOpts.append("testPath=" + encodeURIComponent(options.testPath))
-  elif options.browserChrome:
-    testURL = "about:blank"
-
-  # allow relative paths for logFile
-  if options.logFile:
-    options.logFile = getFullPath(options.logFile)
-  if options.browserChrome:
-    makeTestConfig(options)
-  else:
-    if options.autorun:
-      urlOpts.append("autorun=1")
-    if options.timeout:
-      urlOpts.append("timeout=%d" % options.timeout)
-    if options.closeWhenDone:
-      urlOpts.append("closeWhenDone=1")
-    if options.logFile:
-      urlOpts.append("logFile=" + encodeURIComponent(options.logFile))
-      urlOpts.append("fileLevel=" + encodeURIComponent(options.fileLevel))
-    if options.consoleLevel:
-      urlOpts.append("consoleLevel=" + encodeURIComponent(options.consoleLevel))
-    if options.totalChunks:
-        urlOpts.append("totalChunks=%d" % options.totalChunks)
-        urlOpts.append("thisChunk=%d" % options.thisChunk)
-        if options.chunkByDir:
-            urlOpts.append("chunkByDir=%d" % options.chunkByDir)
-    if options.shuffle:
-        urlOpts.append("shuffle=1")
-    if len(urlOpts) > 0:
-      testURL += "?" + "&".join(urlOpts)
-
-  browserEnv["XPCOM_MEM_BLOAT_LOG"] = LEAK_REPORT_FILE
-
-  if options.fatalAssertions:
-    browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
-
-  # run once with -silent to let the extension manager do its thing
-  # and then exit the app
-  automation.log.info("INFO | runtests.py | Performing extension manager registration: start.\n")
-  # Don't care about this |status|: |runApp()| reporting it should be enough.
-  status = automation.runApp(None, browserEnv, options.app,
-                             PROFILE_DIRECTORY, ["-silent"],
-                             utilityPath = options.utilityPath,
-                             xrePath = options.xrePath,
-                             symbolsPath=options.symbolsPath)
-  # We don't care to call |processLeakLog()| for this step.
-  automation.log.info("\nINFO | runtests.py | Performing extension manager registration: end.")
-
-  # Remove the leak detection file so it can't "leak" to the tests run.
-  # The file is not there if leak logging was not enabled in the application build.
-  if os.path.exists(LEAK_REPORT_FILE):
-    os.remove(LEAK_REPORT_FILE)
+    options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
 
-  # then again to actually run mochitest
-  if options.timeout:
-      timeout = options.timeout + 30
-  elif options.autorun:
-      timeout = None
-  else:
-      timeout = 330.0 # default JS harness timeout is 300 seconds
-  automation.log.info("INFO | runtests.py | Running tests: start.\n")
-  status = automation.runApp(testURL, browserEnv, options.app,
-                             PROFILE_DIRECTORY, options.browserArgs,
-                             runSSLTunnel = True,
-                             utilityPath = options.utilityPath,
-                             xrePath = options.xrePath,
-                             certPath=options.certPath,
-                             debuggerInfo=debuggerInfo,
-                             symbolsPath=options.symbolsPath,
-                             timeout = timeout)
-
-  # Server's no longer needed, and perhaps more importantly, anything it might
-  # spew to console shouldn't disrupt the leak information table we print next.
-  server.stop()
-
-  processLeakLog(LEAK_REPORT_FILE, options.leakThreshold)
-  automation.log.info("\nINFO | runtests.py | Running tests: end.")
-
-  # delete the profile and manifest
-  os.remove(manifest)
-
-  # hanging due to non-halting threads is no fun; assume we hit the errors we
-  # were going to hit already and exit.
-  sys.exit(status)
-
-
-
-#######################
-# CONFIGURATION SETUP #
-#######################
-
-def makeTestConfig(options):
-  "Creates a test configuration file for customizing test execution."
-  def boolString(b):
-    if b:
-      return "true"
-    return "false"
-
-  logFile = options.logFile.replace("\\", "\\\\")
-  testPath = options.testPath.replace("\\", "\\\\")
-  content = """\
-({
-  autoRun: %(autorun)s,
-  closeWhenDone: %(closeWhenDone)s,
-  logPath: "%(logPath)s",
-  testPath: "%(testPath)s"
-})""" % {"autorun": boolString(options.autorun),
-         "closeWhenDone": boolString(options.closeWhenDone),
-         "logPath": logFile,
-         "testPath": testPath}
-
-  config = open(os.path.join(PROFILE_DIRECTORY, "testConfig.js"), "w")
-  config.write(content)
-  config.close() 
-
-
-def addChromeToProfile(options):
-  "Adds MochiKit chrome tests to the profile."
-
-  chromedir = os.path.join(PROFILE_DIRECTORY, "chrome")
-  os.mkdir(chromedir)
-
-  chrome = []
-
-  part = """
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */
-toolbar,
-toolbarpalette {
-  background-color: rgb(235, 235, 235) !important;
-}
-toolbar#nav-bar {
-  background-image: none !important;
-}
-"""
-  chrome.append(part)
-
-
-
-  # write userChrome.css
-  chromeFile = open(os.path.join(PROFILE_DIRECTORY, "userChrome.css"), "a")
-  chromeFile.write("".join(chrome))
-  chromeFile.close()
-
-
-  # register our chrome dir
-  chrometestDir = os.path.abspath(".") + "/"
-  if automation.IS_WIN32:
-    chrometestDir = "file:///" + chrometestDir.replace("\\", "/")
-
-
-  (path, leaf) = os.path.split(options.app)
-  manifest = os.path.join(path, "chrome", "mochikit.manifest")
-  manifestFile = open(manifest, "w")
-  manifestFile.write("content mochikit " + chrometestDir + " contentaccessible=yes\n")
-  if options.browserChrome:
-    overlayLine = "overlay " + BROWSER_CHROME_URL + " " \
-                  "chrome://mochikit/content/browser-test-overlay.xul\n"
-    manifestFile.write(overlayLine)
-  manifestFile.close()
-
-  return manifest
-
-def copyExtraFilesToProfile(options):
-  "Copy extra files or dirs specified on the command line to the testing profile."
-  for f in options.extraProfileFiles:
-    abspath = getFullPath(f)
-    dest = os.path.join(PROFILE_DIRECTORY, os.path.basename(abspath))
-    if os.path.isdir(abspath):
-      shutil.copytree(abspath, dest)
-    else:
-      shutil.copy(abspath, dest)
-
-#########
-# DO IT #
-#########
+  sys.exit(mochitest.runTests(options))
 
 if __name__ == "__main__":
   main()
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -17,16 +17,17 @@
 #
 # The Initial Developer of the Original Code is The Mozilla Foundation
 # Portions created by the Initial Developer are Copyright (C) 2009
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
 #  Serge Gautherie <sgautherie.bz@free.fr>
 #  Ted Mielczarek <ted.mielczarek@gmail.com>
+#  Joel Maher <joel.maher@gmail.com>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -40,253 +41,256 @@
 import re, sys, os, os.path, logging, shutil, signal
 from glob import glob
 from optparse import OptionParser
 from subprocess import Popen, PIPE, STDOUT
 from tempfile import mkdtemp
 
 from automationutils import *
 
-# Init logging
-log = logging.getLogger()
-handler = logging.StreamHandler(sys.stdout)
-log.setLevel(logging.INFO)
-log.addHandler(handler)
+class XPCShellTests(object):
+
+  log = logging.getLogger()
+  oldcwd = os.getcwd()
 
-oldcwd = os.getcwd()
+  def __init__(self):
+    # Init logging
+    handler = logging.StreamHandler(sys.stdout)
+    self.log.setLevel(logging.INFO)
+    self.log.addHandler(handler)
 
-def readManifest(manifest):
-  """Given a manifest file containing a list of test directories,
-  return a list of absolute paths to the directories contained within."""
-  manifestdir = os.path.dirname(manifest)
-  testdirs = []
-  try:
-    f = open(manifest, "r")
-    for line in f:
-      dir = line.rstrip()
-      path = os.path.join(manifestdir, dir)
-      if os.path.isdir(path):
-        testdirs.append(path)
-    f.close()
-  except:
-    pass # just eat exceptions
-  return testdirs
+  def readManifest(self, manifest):
+    """Given a manifest file containing a list of test directories,
+    return a list of absolute paths to the directories contained within."""
+    manifestdir = os.path.dirname(manifest)
+    testdirs = []
+    try:
+      f = open(manifest, "r")
+      for line in f:
+        dir = line.rstrip()
+        path = os.path.join(manifestdir, dir)
+        if os.path.isdir(path):
+          testdirs.append(path)
+      f.close()
+    except:
+      pass # just eat exceptions
+    return testdirs
 
-def runTests(xpcshell, xrePath=None, symbolsPath=None,
-             manifest=None, testdirs=[], testPath=None,
-             interactive=False, logfiles=True,
-             debuggerInfo=None):
-  """Run xpcshell tests.
+  def runTests(self, xpcshell, xrePath=None, symbolsPath=None,
+               manifest=None, testdirs=[], testPath=None,
+               interactive=False, logfiles=True,
+               debuggerInfo=None):
+    """Run xpcshell tests.
 
-  |xpcshell|, is the xpcshell executable to use to run the tests.
-  |xrePath|, if provided, is the path to the XRE to use.
-  |symbolsPath|, if provided is the path to a directory containing
-    breakpad symbols for processing crashes in tests.
-  |manifest|, if provided, is a file containing a list of
-    test directories to run.
-  |testdirs|, if provided, is a list of absolute paths of test directories.
-    No-manifest only option.
-  |testPath|, if provided, indicates a single path and/or test to run.
-  |interactive|, if set to True, indicates to provide an xpcshell prompt
-    instead of automatically executing the test.
-  |logfiles|, if set to False, indicates not to save output to log files.
-    Non-interactive only option.
-  |debuggerInfo|, if set, specifies the debugger and debugger arguments
-    that will be used to launch xpcshell.
-  """
+    |xpcshell|, is the xpcshell executable to use to run the tests.
+    |xrePath|, if provided, is the path to the XRE to use.
+    |symbolsPath|, if provided is the path to a directory containing
+      breakpad symbols for processing crashes in tests.
+    |manifest|, if provided, is a file containing a list of
+      test directories to run.
+    |testdirs|, if provided, is a list of absolute paths of test directories.
+      No-manifest only option.
+    |testPath|, if provided, indicates a single path and/or test to run.
+    |interactive|, if set to True, indicates to provide an xpcshell prompt
+      instead of automatically executing the test.
+    |logfiles|, if set to False, indicates not to save output to log files.
+      Non-interactive only option.
+    |debuggerInfo|, if set, specifies the debugger and debugger arguments
+      that will be used to launch xpcshell.
+    """
 
-  if not testdirs and not manifest:
-    # nothing to test!
-    print >>sys.stderr, "Error: No test dirs or test manifest specified!"
-    return False
+    if not testdirs and not manifest:
+      # nothing to test!
+      print >>sys.stderr, "Error: No test dirs or test manifest specified!"
+      return False
 
-  passCount = 0
-  failCount = 0
+    passCount = 0
+    failCount = 0
 
-  testharnessdir = os.path.dirname(os.path.abspath(__file__))
-  xpcshell = os.path.abspath(xpcshell)
-  # we assume that httpd.js lives in components/ relative to xpcshell
-  httpdJSPath = os.path.join(os.path.dirname(xpcshell), "components", "httpd.js").replace("\\", "/");
+    testharnessdir = os.path.dirname(os.path.abspath(__file__))
+    xpcshell = os.path.abspath(xpcshell)
+    # we assume that httpd.js lives in components/ relative to xpcshell
+    httpdJSPath = os.path.join(os.path.dirname(xpcshell), "components", "httpd.js").replace("\\", "/");
 
-  env = dict(os.environ)
-  # Make assertions fatal
-  env["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
-  # Don't launch the crash reporter client
-  env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
+    env = dict(os.environ)
+    # Make assertions fatal
+    env["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
+    # Don't launch the crash reporter client
+    env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
 
-  if xrePath is None:
-    xrePath = os.path.dirname(xpcshell)
-  else:
-    xrePath = os.path.abspath(xrePath)
-  if sys.platform == 'win32':
-    env["PATH"] = env["PATH"] + ";" + xrePath
-  elif sys.platform in ('os2emx', 'os2knix'):
-    os.environ["BEGINLIBPATH"] = xrePath + ";" + env["BEGINLIBPATH"]
-    os.environ["LIBPATHSTRICT"] = "T"
-  elif sys.platform == 'osx':
-    env["DYLD_LIBRARY_PATH"] = xrePath
-  else: # unix or linux?
-    env["LD_LIBRARY_PATH"] = xrePath
+    if xrePath is None:
+      xrePath = os.path.dirname(xpcshell)
+    else:
+      xrePath = os.path.abspath(xrePath)
+    if sys.platform == 'win32':
+      env["PATH"] = env["PATH"] + ";" + xrePath
+    elif sys.platform in ('os2emx', 'os2knix'):
+      os.environ["BEGINLIBPATH"] = xrePath + ";" + env["BEGINLIBPATH"]
+      os.environ["LIBPATHSTRICT"] = "T"
+    elif sys.platform == 'osx':
+      env["DYLD_LIBRARY_PATH"] = xrePath
+    else: # unix or linux?
+      env["LD_LIBRARY_PATH"] = xrePath
 
-  # xpcsRunArgs: <head.js> function to call to run the test.
-  # pStdout, pStderr: Parameter values for later |Popen()| call.
-  if interactive:
-    xpcsRunArgs = [
+    # xpcsRunArgs: <head.js> function to call to run the test.
+    # pStdout, pStderr: Parameter values for later |Popen()| call.
+    if interactive:
+      xpcsRunArgs = [
       '-e', 'print("To start the test, type |_execute_test();|.");',
       '-i']
-    pStdout = None
-    pStderr = None
-  else:
-    xpcsRunArgs = ['-e', '_execute_test();']
-    if (debuggerInfo and debuggerInfo["interactive"]):
       pStdout = None
       pStderr = None
     else:
-      if sys.platform == 'os2emx':
+      xpcsRunArgs = ['-e', '_execute_test();']
+      if (debuggerInfo and debuggerInfo["interactive"]):
         pStdout = None
-      else:
-        pStdout = PIPE
-      pStderr = STDOUT
-
-  # <head.js> has to be loaded by xpchell: it can't load itself.
-  xpcsCmd = [xpcshell, '-g', xrePath, '-j', '-s'] + \
-            ['-e', 'const _HTTPD_JS_PATH = "%s";' % httpdJSPath,
-             '-f', os.path.join(testharnessdir, 'head.js')]
-
-  if debuggerInfo:
-    xpcsCmd = [debuggerInfo["path"]] + debuggerInfo["args"] + xpcsCmd
-
-  # |testPath| will be the optional path only, or |None|.
-  # |singleFile| will be the optional test only, or |None|.
-  singleFile = None
-  if testPath:
-    if testPath.endswith('.js'):
-      # Split into path and file.
-      if testPath.find('/') == -1:
-        # Test only.
-        singleFile = testPath
-        testPath = None
+        pStderr = None
       else:
-        # Both path and test.
-        # Reuse |testPath| temporarily.
-        testPath = testPath.rsplit('/', 1)
-        singleFile = testPath[1]
-        testPath = testPath[0]
-    else:
-      # Path only.
-      # Simply remove optional ending separator.
-      testPath = testPath.rstrip("/")
+        if sys.platform == 'os2emx':
+          pStdout = None
+        else:
+          pStdout = PIPE
+        pStderr = STDOUT
 
-  # Override testdirs.
-  if manifest is not None:
-    testdirs = readManifest(os.path.abspath(manifest))
+    # <head.js> has to be loaded by xpchell: it can't load itself.
+    xpcsCmd = [xpcshell, '-g', xrePath, '-j', '-s'] + \
+              ['-e', 'const _HTTPD_JS_PATH = "%s";' % httpdJSPath,
+              '-f', os.path.join(testharnessdir, 'head.js')]
 
-  # Process each test directory individually.
-  for testdir in testdirs:
-    if testPath and not testdir.endswith(testPath):
-      continue
+    if debuggerInfo:
+      xpcsCmd = [debuggerInfo["path"]] + debuggerInfo["args"] + xpcsCmd
 
-    testdir = os.path.abspath(testdir)
+    # |testPath| will be the optional path only, or |None|.
+    # |singleFile| will be the optional test only, or |None|.
+    singleFile = None
+    if testPath:
+      if testPath.endswith('.js'):
+        # Split into path and file.
+        if testPath.find('/') == -1:
+          # Test only.
+          singleFile = testPath
+          testPath = None
+        else:
+          # Both path and test.
+          # Reuse |testPath| temporarily.
+          testPath = testPath.rsplit('/', 1)
+          singleFile = testPath[1]
+          testPath = testPath[0]
+      else:
+        # Path only.
+        # Simply remove optional ending separator.
+        testPath = testPath.rstrip("/")
 
-    # get the list of head and tail files from the directory
-    testHeadFiles = []
-    for f in sorted(glob(os.path.join(testdir, "head_*.js"))):
-      if os.path.isfile(f):
-        testHeadFiles += [f]
-    testTailFiles = []
-    # Tails are executed in the reverse order, to "match" heads order,
-    # as in "h1-h2-h3 then t3-t2-t1".
-    for f in reversed(sorted(glob(os.path.join(testdir, "tail_*.js")))):
-      if os.path.isfile(f):
-        testTailFiles += [f]
+    # Override testdirs.
+    if manifest is not None:
+      testdirs = self.readManifest(os.path.abspath(manifest))
 
-    # if a single test file was specified, we only want to execute that test
-    testfiles = sorted(glob(os.path.join(testdir, "test_*.js")))
-    if singleFile:
-      if singleFile in [os.path.basename(x) for x in testfiles]:
-        testfiles = [os.path.join(testdir, singleFile)]
-      else: # not in this dir? skip it
+    # Process each test directory individually.
+    for testdir in testdirs:
+      if testPath and not testdir.endswith(testPath):
         continue
 
-    cmdH = ", ".join(['"' + f.replace('\\', '/') + '"'
-                       for f in testHeadFiles])
-    cmdT = ", ".join(['"' + f.replace('\\', '/') + '"'
-                       for f in testTailFiles])
-    cmdH = xpcsCmd + \
-           ['-e', 'const _HEAD_FILES = [%s];' % cmdH] + \
-           ['-e', 'const _TAIL_FILES = [%s];' % cmdT]
+      testdir = os.path.abspath(testdir)
+
+      # get the list of head and tail files from the directory
+      testHeadFiles = []
+      for f in sorted(glob(os.path.join(testdir, "head_*.js"))):
+        if os.path.isfile(f):
+          testHeadFiles += [f]
+      testTailFiles = []
+      # Tails are executed in the reverse order, to "match" heads order,
+      # as in "h1-h2-h3 then t3-t2-t1".
+      for f in reversed(sorted(glob(os.path.join(testdir, "tail_*.js")))):
+        if os.path.isfile(f):
+          testTailFiles += [f]
 
-    # Now execute each test individually.
-    for test in testfiles:
-      # The test file will have to be loaded after the head files.
-      cmdT = ['-e', 'const _TEST_FILE = ["%s"];' %
-                      os.path.join(testdir, test).replace('\\', '/')]
+      # if a single test file was specified, we only want to execute that test
+      testfiles = sorted(glob(os.path.join(testdir, "test_*.js")))
+      if singleFile:
+        if singleFile in [os.path.basename(x) for x in testfiles]:
+          testfiles = [os.path.join(testdir, singleFile)]
+        else: # not in this dir? skip it
+          continue
 
-      # create a temp dir that the JS harness can stick a profile in
-      profileDir = None
-      try:
-        profileDir = mkdtemp()
-        env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir
+      cmdH = ", ".join(['"' + f.replace('\\', '/') + '"'
+                 for f in testHeadFiles])
+      cmdT = ", ".join(['"' + f.replace('\\', '/') + '"'
+                 for f in testTailFiles])
+      cmdH = xpcsCmd + \
+                ['-e', 'const _HEAD_FILES = [%s];' % cmdH] + \
+                ['-e', 'const _TAIL_FILES = [%s];' % cmdT]
 
-        # Enable leaks (only) detection to its own log file.
-        leakLogFile = os.path.join(profileDir, "runxpcshelltests_leaks.log")
-        env["XPCOM_MEM_LEAK_LOG"] = leakLogFile
+      # Now execute each test individually.
+      for test in testfiles:
+        # The test file will have to be loaded after the head files.
+        cmdT = ['-e', 'const _TEST_FILE = ["%s"];' %
+                os.path.join(testdir, test).replace('\\', '/')]
 
-        proc = Popen(cmdH + cmdT + xpcsRunArgs,
-                     stdout=pStdout, stderr=pStderr, env=env, cwd=testdir)
+        # create a temp dir that the JS harness can stick a profile in
+        profileDir = None
+        try:
+          profileDir = mkdtemp()
+          env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir
+
+          # Enable leaks (only) detection to its own log file.
+          leakLogFile = os.path.join(profileDir, "runxpcshelltests_leaks.log")
+          env["XPCOM_MEM_LEAK_LOG"] = leakLogFile
 
-        # allow user to kill hung subprocess with SIGINT w/o killing this script
-        # - don't move this line above Popen, or child will inherit the SIG_IGN
-        signal.signal(signal.SIGINT, signal.SIG_IGN)
-        # |stderr == None| as |pStderr| was either |None| or redirected to |stdout|.
-        stdout, stderr = proc.communicate()
-        signal.signal(signal.SIGINT, signal.SIG_DFL)
+          proc = Popen(cmdH + cmdT + xpcsRunArgs,
+                      stdout=pStdout, stderr=pStderr, env=env, cwd=testdir)
 
-        if interactive:
-          # Not sure what else to do here...
-          return True
+          # allow user to kill hung subprocess with SIGINT w/o killing this script
+          # - don't move this line above Popen, or child will inherit the SIG_IGN
+          signal.signal(signal.SIGINT, signal.SIG_IGN)
+          # |stderr == None| as |pStderr| was either |None| or redirected to |stdout|.
+          stdout, stderr = proc.communicate()
+          signal.signal(signal.SIGINT, signal.SIG_DFL)
 
-        if proc.returncode != 0 or (stdout and re.search("^TEST-UNEXPECTED-FAIL", stdout, re.MULTILINE)):
-          print """TEST-UNEXPECTED-FAIL | %s | test failed (with xpcshell return code: %d), see following log:
+          if interactive:
+            # Not sure what else to do here...
+            return True
+
+          if proc.returncode != 0 or (stdout and re.search("^TEST-UNEXPECTED-FAIL", stdout, re.MULTILINE)):
+            print """TEST-UNEXPECTED-FAIL | %s | test failed (with xpcshell return code: %d), see following log:
   >>>>>>>
   %s
   <<<<<<<""" % (test, proc.returncode, stdout)
-          checkForCrashes(testdir, symbolsPath, testName=test)
-          failCount += 1
-        else:
-          print "TEST-PASS | %s | test passed" % test
-          passCount += 1
+            checkForCrashes(testdir, symbolsPath, testName=test)
+            failCount += 1
+          else:
+            print "TEST-PASS | %s | test passed" % test
+            passCount += 1
 
-        dumpLeakLog(leakLogFile, True)
+          dumpLeakLog(leakLogFile, True)
 
-        if logfiles and stdout:
-          try:
-            f = open(test + ".log", "w")
-            f.write(stdout)
+          if logfiles and stdout:
+            try:
+              f = open(test + ".log", "w")
+              f.write(stdout)
 
-            if os.path.exists(leakLogFile):
-              leaks = open(leakLogFile, "r")
-              f.write(leaks.read())
-              leaks.close()
-          finally:
-            if f:
-              f.close()
-      finally:
-        if profileDir:
-          shutil.rmtree(profileDir)
+              if os.path.exists(leakLogFile):
+                leaks = open(leakLogFile, "r")
+                f.write(leaks.read())
+                leaks.close()
+            finally:
+              if f:
+                f.close()
+        finally:
+          if profileDir:
+            shutil.rmtree(profileDir)
 
-  if passCount == 0 and failCount == 0:
-    print "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | No tests run. Did you pass an invalid --test-path?"
-    failCount = 1
+    if passCount == 0 and failCount == 0:
+      print "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | No tests run. Did you pass an invalid --test-path?"
+      failCount = 1
 
-  print """INFO | Result summary:
+    print """INFO | Result summary:
 INFO | Passed: %d
 INFO | Failed: %d""" % (passCount, failCount)
 
-  return failCount == 0
+    return failCount == 0
 
 def main():
   """Process command line arguments and call runTests() to do the real work."""
   parser = OptionParser()
 
   addCommonOptions(parser)
   parser.add_option("--interactive",
                     action="store_true", dest="interactive", default=False,
@@ -302,33 +306,35 @@ def main():
                     help="don't create log files")
   parser.add_option("--test-path",
                     type="string", dest="testPath", default=None,
                     help="single path and/or test filename to test")
   options, args = parser.parse_args()
 
   if len(args) < 2 and options.manifest is None or \
      (len(args) < 1 and options.manifest is not None):
-    print >>sys.stderr, """Usage: %s <path to xpcshell> <test dirs>
-  or: %s --manifest=test.manifest <path to xpcshell>""" % (sys.argv[0],
+     print >>sys.stderr, """Usage: %s <path to xpcshell> <test dirs>
+           or: %s --manifest=test.manifest <path to xpcshell>""" % (sys.argv[0],
                                                            sys.argv[0])
-    sys.exit(1)
+     sys.exit(1)
 
-  debuggerInfo = getDebuggerInfo(oldcwd, options.debugger, options.debuggerArgs,
+  xpcsh = XPCShellTests()
+  debuggerInfo = getDebuggerInfo(xpcsh.oldcwd, options.debugger, options.debuggerArgs,
     options.debuggerInteractive);
 
   if options.interactive and not options.testPath:
     print >>sys.stderr, "Error: You must specify a test filename in interactive mode!"
     sys.exit(1)
 
-  if not runTests(args[0],
-                  xrePath=options.xrePath,
-                  symbolsPath=options.symbolsPath,
-                  manifest=options.manifest,
-                  testdirs=args[1:],
-                  testPath=options.testPath,
-                  interactive=options.interactive,
-                  logfiles=options.logfiles,
-                  debuggerInfo=debuggerInfo):
+    
+  if not xpcsh.runTests(args[0],
+                        xrePath=options.xrePath,
+                        symbolsPath=options.symbolsPath,
+                        manifest=options.manifest,
+                        testdirs=args[1:],
+                        testPath=options.testPath,
+                        interactive=options.interactive,
+                        logfiles=options.logfiles,
+                        debuggerInfo=debuggerInfo):
     sys.exit(1)
 
 if __name__ == '__main__':
   main()