Bug 530475 - Refactor test harnesses to classes for easier reuse for mobile testing p=jmaher r=ted
☠☠ backed out by 961400b8b0b6 ☠ ☠
authorJonathan Griffin <jgriffin@mozilla.com>
Fri, 08 Jan 2010 12:05:45 -0800
changeset 36960 ca10f6b27b95898f9ee7e066128578e4a67c6e9d
parent 36959 36bd078ee596ed56c70272f58d929f1621bbfa75
child 36961 256c51e15d679219d3efd186ac5055bf9abb4437
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted
bugs530475
milestone1.9.3a1pre
Bug 530475 - Refactor test harnesses to classes for easier reuse for mobile testing p=jmaher r=ted
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
@@ -48,105 +48,35 @@ import shutil
 import signal
 import subprocess
 import sys
 import threading
 import tempfile
 
 from automationutils import checkForCrashes
 
-"""
-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])))
-
-__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 +97,164 @@ 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(__file__)))
+
+  # 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,400 @@ 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_WINDOW_CRASH_DIALOG'] = '1'
+    return env
 
-if IS_WIN32:
-  import ctypes, time, msvcrt
-  PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
-  GetLastError = ctypes.windll.kernel32.GetLastError
+  if IS_WIN32:
+    ctypes = __import__('ctypes')
+    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.ctypes.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 = None, 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 == None:
+      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 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()