Bug 597064 - Add timeout logic to xpcshell test runner. r=ted
authorJosh Matthews <josh@joshmatthews.net>
Thu, 20 Dec 2012 03:43:19 -0500
changeset 142307 4a1cbbb07ce23516c12662db6b154f95439bbca0
parent 142306 5d965d4738971517722400c7db36aa87b82d048c
child 142308 6c48ce88a31ad3476e3403008a02b589ac44e2ba
child 142327 98e5c35041c0086535b46e8a159d2445c5973721
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted
bugs597064
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 597064 - Add timeout logic to xpcshell test runner. r=ted
build/automation.py.in
build/macosx/universal/flight.mk
config/makefiles/xpcshell.mk
js/src/config/makefiles/xpcshell.mk
testing/testsuite-targets.mk
testing/xpcshell/Makefile.in
testing/xpcshell/mach_commands.py
testing/xpcshell/runxpcshelltests.py
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -78,23 +78,31 @@ except:
 
 
 if _IS_WIN32:
   import ctypes, ctypes.wintypes, time, msvcrt
 else:
   import errno
 
 
+def getGlobalLog():
+  return _log
+
+def resetGlobalLog(log):
+  while _log.handlers:
+    _log.removeHandler(_log.handlers[0])
+  handler = logging.StreamHandler(log)
+  _log.setLevel(logging.INFO)
+  _log.addHandler(handler)
+
 # 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)
+resetGlobalLog(sys.stdout)
 
 
 #################
 # PROFILE SETUP #
 #################
 
 class SyntaxError(Exception):
   "Signifies a syntax error on a particular line in server-locations.txt."
@@ -900,20 +908,24 @@ user_pref("camino.use_system_proxy_setti
     except IOError, err:
         self.log.info("Failed to read image from %s", imgoutput)
 
     import base64
     encoded = base64.b64encode(image)
     self.log.info("SCREENSHOT: data:image/png;base64,%s", encoded)
 
   def killAndGetStack(self, processPID, utilityPath, debuggerInfo):
-    """Kill the process, preferrably in a way that gets us a stack trace."""
+    """Kill the process, preferrably in a way that gets us a stack trace.
+       Also attempts to obtain a screenshot before killing the process."""
     if not debuggerInfo:
       self.dumpScreen(utilityPath)
+    self.killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo)
 
+  def killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo):
+    """Kill the process, preferrably in a way that gets us a stack trace."""
     if self.CRASHREPORTER and not debuggerInfo:
       if self.UNIXISH:
         # ABRT will get picked up by Breakpad's signal handler
         os.kill(processPID, signal.SIGABRT)
         return
       elif self.IS_WIN32:
         # We should have a "crashinject" program in our utility path
         crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
--- a/build/macosx/universal/flight.mk
+++ b/build/macosx/universal/flight.mk
@@ -39,16 +39,18 @@ ifdef ENABLE_TESTS
 # automation.py differs because it hardcodes a path to
 # dist/bin. It doesn't matter which one we use.
 	if test -d $(DIST_ARCH_1)/test-package-stage -a                 \
                 -d $(DIST_ARCH_2)/test-package-stage; then              \
            cp $(DIST_ARCH_1)/test-package-stage/mochitest/automation.py \
              $(DIST_ARCH_2)/test-package-stage/mochitest/;              \
            cp -RL $(DIST_ARCH_1)/test-package-stage/mochitest/extensions/specialpowers \
              $(DIST_ARCH_2)/test-package-stage/mochitest/extensions/;              \
+           cp $(DIST_ARCH_1)/test-package-stage/xpcshell/automation.py  \
+             $(DIST_ARCH_2)/test-package-stage/xpcshell/;               \
            cp $(DIST_ARCH_1)/test-package-stage/reftest/automation.py   \
              $(DIST_ARCH_2)/test-package-stage/reftest/;                \
            cp -RL $(DIST_ARCH_1)/test-package-stage/reftest/specialpowers \
              $(DIST_ARCH_2)/test-package-stage/reftest/;              \
            $(TOPSRCDIR)/build/macosx/universal/unify                 \
              --unify-with-sort "\.manifest$$" \
              --unify-with-sort "all-test-dirs\.list$$"               \
              $(DIST_ARCH_1)/test-package-stage                          \
--- a/config/makefiles/xpcshell.mk
+++ b/config/makefiles/xpcshell.mk
@@ -35,64 +35,69 @@ ifndef NO_XPCSHELL_MANIFEST_CHECK #{
 	  $(addprefix $(MOZILLA_DIR)/$(relativesrcdir)/,$(XPCSHELL_TESTS))
 endif #} NO_XPCSHELL_MANIFEST_CHECK 
 
 ###########################################################################
 # Execute all tests in the $(XPCSHELL_TESTS) directories.
 # See also testsuite-targets.mk 'xpcshell-tests' target for global execution.
 xpcshell-tests:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
       -I$(DEPTH)/_tests/mozbase/mozinfo \
 	  $(testxpcsrcdir)/runxpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --tests-root-dir=$(testxpcobjdir) \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
 	  --xunit-file=$(testxpcobjdir)/$(relativesrcdir)/results.xml \
 	  --xunit-suite-name=xpcshell \
 	  --test-plugin-path=$(DIST)/plugins \
 	  $(EXTRA_TEST_ARGS) \
 	  $(LIBXUL_DIST)/bin/xpcshell \
 	  $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(relativesrcdir)/$(dir))
 
 xpcshell-tests-remote: DM_TRANS?=adb
 xpcshell-tests-remote:
-	$(PYTHON) -u $(topsrcdir)/testing/xpcshell/remotexpcshelltests.py \
+	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
+	  $(topsrcdir)/testing/xpcshell/remotexpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
 	  $(EXTRA_TEST_ARGS) \
 	  --dm_trans=$(DM_TRANS) \
 	  --deviceIP=${TEST_DEVICE} \
 	  --objdir=$(DEPTH) \
 	  $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(relativesrcdir)/$(dir))
 
 ###########################################################################
 # Execute a single test, specified in $(SOLO_FILE), but don't automatically
 # start the test. Instead, present the xpcshell prompt so the user can
 # attach a debugger and then start the test.
 check-interactive:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
       -I$(DEPTH)/_tests/mozbase/mozinfo \
 	  $(testxpcsrcdir)/runxpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --test-path=$(SOLO_FILE) \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
 	  --profile-name=$(MOZ_APP_NAME) \
 	  --test-plugin-path=$(DIST)/plugins \
 	  --interactive \
 	  $(LIBXUL_DIST)/bin/xpcshell \
 	  $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(relativesrcdir)/$(dir))
 
 # Execute a single test, specified in $(SOLO_FILE)
 check-one:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
       -I$(DEPTH)/_tests/mozbase/mozinfo \
 	  $(testxpcsrcdir)/runxpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --test-path=$(SOLO_FILE) \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
 	  --profile-name=$(MOZ_APP_NAME) \
@@ -100,16 +105,17 @@ check-one:
 	  --verbose \
 	  $(EXTRA_TEST_ARGS) \
 	  $(LIBXUL_DIST)/bin/xpcshell \
 	  $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(relativesrcdir)/$(dir))
 
 check-one-remote: DM_TRANS?=adb
 check-one-remote:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
 	  -I$(topsrcdir)/build/mobile \
 	  -I$(topsrcdir)/testing/mozbase/mozdevice/mozdevice \
 	  $(testxpcsrcdir)/remotexpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --test-path=$(SOLO_FILE) \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
--- a/js/src/config/makefiles/xpcshell.mk
+++ b/js/src/config/makefiles/xpcshell.mk
@@ -35,64 +35,69 @@ ifndef NO_XPCSHELL_MANIFEST_CHECK #{
 	  $(addprefix $(MOZILLA_DIR)/$(relativesrcdir)/,$(XPCSHELL_TESTS))
 endif #} NO_XPCSHELL_MANIFEST_CHECK 
 
 ###########################################################################
 # Execute all tests in the $(XPCSHELL_TESTS) directories.
 # See also testsuite-targets.mk 'xpcshell-tests' target for global execution.
 xpcshell-tests:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
       -I$(DEPTH)/_tests/mozbase/mozinfo \
 	  $(testxpcsrcdir)/runxpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --tests-root-dir=$(testxpcobjdir) \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
 	  --xunit-file=$(testxpcobjdir)/$(relativesrcdir)/results.xml \
 	  --xunit-suite-name=xpcshell \
 	  --test-plugin-path=$(DIST)/plugins \
 	  $(EXTRA_TEST_ARGS) \
 	  $(LIBXUL_DIST)/bin/xpcshell \
 	  $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(relativesrcdir)/$(dir))
 
 xpcshell-tests-remote: DM_TRANS?=adb
 xpcshell-tests-remote:
-	$(PYTHON) -u $(topsrcdir)/testing/xpcshell/remotexpcshelltests.py \
+	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
+	  $(topsrcdir)/testing/xpcshell/remotexpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
 	  $(EXTRA_TEST_ARGS) \
 	  --dm_trans=$(DM_TRANS) \
 	  --deviceIP=${TEST_DEVICE} \
 	  --objdir=$(DEPTH) \
 	  $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(relativesrcdir)/$(dir))
 
 ###########################################################################
 # Execute a single test, specified in $(SOLO_FILE), but don't automatically
 # start the test. Instead, present the xpcshell prompt so the user can
 # attach a debugger and then start the test.
 check-interactive:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
       -I$(DEPTH)/_tests/mozbase/mozinfo \
 	  $(testxpcsrcdir)/runxpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --test-path=$(SOLO_FILE) \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
 	  --profile-name=$(MOZ_APP_NAME) \
 	  --test-plugin-path=$(DIST)/plugins \
 	  --interactive \
 	  $(LIBXUL_DIST)/bin/xpcshell \
 	  $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(relativesrcdir)/$(dir))
 
 # Execute a single test, specified in $(SOLO_FILE)
 check-one:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
       -I$(DEPTH)/_tests/mozbase/mozinfo \
 	  $(testxpcsrcdir)/runxpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --test-path=$(SOLO_FILE) \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
 	  --profile-name=$(MOZ_APP_NAME) \
@@ -100,16 +105,17 @@ check-one:
 	  --verbose \
 	  $(EXTRA_TEST_ARGS) \
 	  $(LIBXUL_DIST)/bin/xpcshell \
 	  $(foreach dir,$(XPCSHELL_TESTS),$(testxpcobjdir)/$(relativesrcdir)/$(dir))
 
 check-one-remote: DM_TRANS?=adb
 check-one-remote:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
 	  -I$(topsrcdir)/build/mobile \
 	  -I$(topsrcdir)/testing/mozbase/mozdevice/mozdevice \
 	  $(testxpcsrcdir)/remotexpcshelltests.py \
 	  --symbols-path=$(DIST)/crashreporter-symbols \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --test-path=$(SOLO_FILE) \
 	  --testing-modules-dir=$(DEPTH)/_tests/modules \
--- a/testing/testsuite-targets.mk
+++ b/testing/testsuite-targets.mk
@@ -266,32 +266,35 @@ jstestbrowser:
 
 GARBAGE += $(addsuffix .log,$(MOCHITESTS) reftest crashtest jstestbrowser)
 
 # Execute all xpcshell tests in the directories listed in the manifest.
 # See also config/rules.mk 'xpcshell-tests' target for local execution.
 # Usage: |make [TEST_PATH=...] [EXTRA_TEST_ARGS=...] xpcshell-tests|.
 xpcshell-tests:
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
-	  -I$(topsrcdir)/build -I$(DEPTH)/_tests/mozbase/mozinfo \
+	  -I$(DEPTH)/build \
+	  -I$(topsrcdir)/build \
+	  -I$(DEPTH)/_tests/mozbase/mozinfo \
 	  $(topsrcdir)/testing/xpcshell/runxpcshelltests.py \
 	  --manifest=$(DEPTH)/_tests/xpcshell/xpcshell.ini \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --no-logfiles \
 	  --tests-root-dir=$(call core_abspath,_tests/xpcshell) \
 	  --testing-modules-dir=$(call core_abspath,_tests/modules) \
 	  --xunit-file=$(call core_abspath,_tests/xpcshell/results.xml) \
 	  --xunit-suite-name=xpcshell \
           $(SYMBOLS_PATH) \
 	  $(TEST_PATH_ARG) $(EXTRA_TEST_ARGS) \
 	  $(LIBXUL_DIST)/bin/xpcshell
 
 B2G_XPCSHELL = \
 	rm -f ./@.log && \
 	$(PYTHON) -u $(topsrcdir)/config/pythonpath.py \
+	  -I$(DEPTH)/build \
 	  -I$(topsrcdir)/build \
 	  $(topsrcdir)/testing/xpcshell/runtestsb2g.py \
 	  --manifest=$(DEPTH)/_tests/xpcshell/xpcshell.ini \
 	  --build-info-json=$(DEPTH)/mozinfo.json \
 	  --no-logfiles \
 	  --use-device-libs \
 	  --no-clean \
 	  --objdir=$(DEPTH) \
--- a/testing/xpcshell/Makefile.in
+++ b/testing/xpcshell/Makefile.in
@@ -52,19 +52,20 @@ libs::
 	$(INSTALL) $(srcdir)/xpcshell.ini $(DEPTH)/_tests/xpcshell
 	$(INSTALL) $(srcdir)/xpcshell_b2g.ini $(DEPTH)/_tests/xpcshell
 	$(INSTALL) $(srcdir)/xpcshell_android.ini $(DEPTH)/_tests/xpcshell
 	cp $(srcdir)/xpcshell.ini $(DEPTH)/_tests/xpcshell/all-test-dirs.list
 
 # Run selftests
 check::
 	OBJDIR=$(DEPTH) $(PYTHON) $(topsrcdir)/config/pythonpath.py \
-	  -I$(topsrcdir)/build -I$(topsrcdir)/testing/mozbase/mozinfo/mozinfo $(srcdir)/selftest.py
+	  -I$(DEPTH)/build -I$(topsrcdir)/build -I$(topsrcdir)/testing/mozbase/mozinfo/mozinfo $(srcdir)/selftest.py
 
 stage-package:
 	$(NSINSTALL) -D $(PKG_STAGE)/xpcshell/tests
 	@(cd $(topsrcdir)/testing/mozbase/mozinfo/mozinfo && tar $(TAR_CREATE_FLAGS) - $(MOZINFO_FILES)) | (cd $(PKG_STAGE)/xpcshell && tar -xf -)
 	@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_FILES)) | (cd $(PKG_STAGE)/xpcshell && tar -xf -)
 	@(cd $(topsrcdir)/build && tar $(TAR_CREATE_FLAGS) - $(EXTRA_BUILD_FILES)) | (cd $(PKG_STAGE)/xpcshell && tar -xf -)
 	@cp $(DEPTH)/mozinfo.json $(PKG_STAGE)/xpcshell
+	@cp $(DEPTH)/build/automation.py $(PKG_STAGE)/xpcshell
 	@(cd $(topsrcdir)/testing/mozbase/mozdevice/mozdevice && tar $(TAR_CREATE_FLAGS) - $(MOZDEVICE_FILES)) | (cd $(PKG_STAGE)/xpcshell && tar -xf -)
 	(cd $(DEPTH)/_tests/xpcshell/ && tar $(TAR_CREATE_FLAGS_QUIET) - *) | (cd $(PKG_STAGE)/xpcshell/tests && tar -xf -)
 	@(cd $(DIST)/bin/components && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_COMPONENTS)) | (cd $(PKG_STAGE)/bin/components && tar -xf -)
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -42,16 +42,20 @@ class XPCShellRunner(MozbuildObject):
         manifest = os.path.join(self.topobjdir, '_tests', 'xpcshell',
             'xpcshell.ini')
 
         return self._run_xpcshell_harness(manifest=manifest, **kwargs)
 
     def run_test(self, test_file, debug=False, interactive=False,
         keep_going=False, shuffle=False):
         """Runs an individual xpcshell test."""
+        # TODO Bug 794506 remove once mach integrates with virtualenv.
+        build_path = os.path.join(self.topobjdir, 'build')
+        if build_path not in sys.path:
+            sys.path.append(build_path)
 
         if test_file == 'all':
             self.run_suite(debug=debug, interactive=interactive,
                 keep_going=keep_going, shuffle=shuffle)
             return
 
         path_arg = self._wrap_path_argument(test_file)
 
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -5,24 +5,28 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import re, sys, os, os.path, logging, shutil, signal, math, time, traceback
 import xml.dom.minidom
 from glob import glob
 from optparse import OptionParser
 from subprocess import Popen, PIPE, STDOUT
 from tempfile import mkdtemp, gettempdir
+from threading import Timer
 import manifestparser
 import mozinfo
 import random
 import socket
 import time
 
+from automation import Automation, getGlobalLog, resetGlobalLog
 from automationutils import *
 
+HARNESS_TIMEOUT = 5 * 60
+
 # --------------------------------------------------------------
 # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
 #
 here = os.path.dirname(__file__)
 mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase'))
 
 try:
     import mozcrash
@@ -47,24 +51,23 @@ def parse_json(j):
 """ Control-C handling """
 gotSIGINT = False
 def markGotSIGINT(signum, stackFrame):
     global gotSIGINT
     gotSIGINT = True
 
 class XPCShellTests(object):
 
-    log = logging.getLogger()
+    log = getGlobalLog()
     oldcwd = os.getcwd()
 
-    def __init__(self, log=sys.stdout):
+    def __init__(self, log=None):
         """ Init logging and node status """
-        handler = logging.StreamHandler(log)
-        self.log.setLevel(logging.INFO)
-        self.log.addHandler(handler)
+        if log:
+            resetGlobalLog(log)
         self.nodeProc = None
 
     def buildTestList(self):
         """
           read the xpcshell.ini manifest and set self.alltests to be
           an array of test objects.
 
           if we are chunking tests, it will be done here as well
@@ -568,16 +571,20 @@ class XPCShellTests(object):
             testsuite.appendChild(testcase)
 
         testsuite.setAttribute("tests", str(total))
         testsuite.setAttribute("failures", str(failed))
         testsuite.setAttribute("skip", str(skipped))
 
         doc.writexml(fh, addindent="  ", newl="\n", encoding="utf-8")
 
+    def testTimeout(self, test, processPID):
+        self.log.error("TEST-UNEXPECTED-FAIL | %s | Test timed out" % test)
+        Automation().killAndGetStackNoScreenshot(processPID, self.appPath, None)
+
     def post_to_autolog(self, results, name):
         from moztest.results import TestContext, TestResult, TestResultCollection
         from moztest.output.autolog import AutologOutput
 
         context = TestContext(
             testgroup='b2g xpcshell testsuite',
             operating_system='android',
             arch='emulator',
@@ -878,16 +885,21 @@ class XPCShellTests(object):
         cmdT = self.buildCmdTestFile(name)
 
         args = self.xpcsRunArgs[:]
         if 'debug' in test:
             args.insert(0, '-d')
 
         completeCmd = cmdH + cmdT + args
 
+        testTimer = None
+        if not interactive and not self.debuggerInfo:
+            testTimer = Timer(HARNESS_TIMEOUT, lambda: self.testTimeout(name, proc.pid))
+            testTimer.start()
+
         proc = None
 
         try:
             self.log.info("TEST-INFO | %s | running test ..." % name)
             if verbose:
                 self.logCommand(name, completeCmd, test_dir)
 
             startTime = time.time()
@@ -901,17 +913,20 @@ class XPCShellTests(object):
             # - don't move this line above launchProcess, or child will inherit the SIG_IGN
             signal.signal(signal.SIGINT, markGotSIGINT)
             # |stderr == None| as |pStderr| was either |None| or redirected to |stdout|.
             stdout, stderr = self.communicate(proc)
             signal.signal(signal.SIGINT, signal.SIG_DFL)
 
             if interactive:
                 # Not sure what else to do here...
-                return True
+                return True, xunit_result
+
+            if testTimer:
+                testTimer.cancel()
 
             def print_stdout(stdout):
                 """Print stdout line-by-line to avoid overflowing buffers."""
                 self.log.info(">>>>>>>")
                 if (stdout):
                     for line in stdout.splitlines():
                         self.log.info(line)
                 self.log.info("<<<<<<<")