Bug 487494 - Pipe xpcshell's output through a stack fixer.;r=ted
authorChris Manchester <cmanchester@mozilla.com>
Thu, 11 Jun 2015 11:21:13 -0700
changeset 279156 98e985a2d950eef472bc67a7555237f43f326d66
parent 279155 182ac56399c47c0b5df657596db53064b107594d
child 279157 aca8404dc72cc801ef17589d717c74bff0d48333
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted
bugs487494
milestone41.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 487494 - Pipe xpcshell's output through a stack fixer.;r=ted
testing/config/mozharness/linux_config.py
testing/config/mozharness/mac_config.py
testing/config/mozharness/windows_config.py
testing/xpcshell/mach_commands.py
testing/xpcshell/runxpcshelltests.py
testing/xpcshell/selftest.py
--- a/testing/config/mozharness/linux_config.py
+++ b/testing/config/mozharness/linux_config.py
@@ -79,15 +79,16 @@ config = {
             ],
             "run_filename": "runtests.py",
             "testsdir": "mochitest"
         },
         "xpcshell": {
             "options": [
                 "--symbols-path=%(symbols_path)s",
                 "--test-plugin-path=%(test_plugin_path)s",
-                "--log-raw=%(raw_log_file)s"
+                "--log-raw=%(raw_log_file)s",
+                "--utility-path=tests/bin",
             ],
             "run_filename": "runxpcshelltests.py",
             "testsdir": "xpcshell"
         }
     }
 }
--- a/testing/config/mozharness/mac_config.py
+++ b/testing/config/mozharness/mac_config.py
@@ -77,15 +77,16 @@ config = {
             ],
             "run_filename": "runtests.py",
             "testsdir": "mochitest"
         },
         "xpcshell": {
             "options": [
                 "--symbols-path=%(symbols_path)s",
                 "--test-plugin-path=%(test_plugin_path)s",
-                "--log-raw=%(raw_log_file)s"
+                "--log-raw=%(raw_log_file)s",
+                "--utility-path=tests/bin",
             ],
             "run_filename": "runxpcshelltests.py",
             "testsdir": "xpcshell"
         }
     }
 }
--- a/testing/config/mozharness/windows_config.py
+++ b/testing/config/mozharness/windows_config.py
@@ -77,15 +77,16 @@ config = {
             ],
             "run_filename": "runtests.py",
             "testsdir": "mochitest"
         },
         "xpcshell": {
             "options": [
                 "--symbols-path=%(symbols_path)s",
                 "--test-plugin-path=%(test_plugin_path)s",
-                "--log-raw=%(raw_log_file)s"
+                "--log-raw=%(raw_log_file)s",
+                "--utility-path=tests/bin",
             ],
             "run_filename": "runxpcshelltests.py",
             "testsdir": "xpcshell"
         }
     }
 }
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -168,16 +168,17 @@ class XPCShellRunner(MozbuildObject):
             'xunitName': 'xpcshell',
             'pluginsPath': os.path.join(self.distdir, 'plugins'),
             'debugger': debugger,
             'debuggerArgs': debuggerArgs,
             'debuggerInteractive': debuggerInteractive,
             'jsDebugger': jsDebugger,
             'jsDebuggerPort': jsDebuggerPort,
             'test_tags': test_tags,
+            'utility_path': self.bindir,
         }
 
         if test_path is not None:
             args['testPath'] = test_path
 
         # A failure manifest is written by default. If --rerun-failures is
         # specified and a prior failure manifest is found, the prior manifest
         # will be run. A new failure manifest is always written over any
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -1,15 +1,16 @@
 #!/usr/bin/env python
 #
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import copy
+import importlib
 import json
 import math
 import mozdebug
 import mozinfo
 import os
 import os.path
 import random
 import re
@@ -85,16 +86,49 @@ def cleanup_encoding(s):
        sanitized unicode object."""
     if not isinstance(s, basestring):
         return unicode(s)
     if not isinstance(s, unicode):
         s = s.decode('utf-8', 'replace')
     # Replace all C0 and C1 control characters with \xNN escapes.
     return _cleanup_encoding_re.sub(_cleanup_encoding_repl, s)
 
+def find_stack_fixer(mozinfo, utility_dir, symbols_path):
+    # This is mostly taken from the equivalent in runreftest.py, itself similar
+    # to the mochitest version. It's not a huge amount of code, but deduping it
+    # might be nice. This version is indepenent of an enclosing harness class,
+    # so should easily be movable to a shared location.
+    if not mozinfo.info.get('debug'):
+        return None
+
+    def import_stack_fixer_module(module_name):
+        sys.path.insert(0, utility_dir)
+        module = importlib.import_module(module_name)
+        sys.path.pop(0)
+        return module
+
+    stack_fixer_function = None
+
+    if symbols_path and os.path.exists(symbols_path):
+        # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files).
+        # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
+        stack_fixer_module = import_stack_fixer_module('fix_stack_using_bpsyms')
+        stack_fixer_function = lambda line: stack_fixer_module.fixSymbols(line, symbols_path)
+    elif mozinfo.isMac:
+        # Run each line through fix_macosx_stack.py (uses atos).
+        # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
+        stack_fixer_module = import_stack_fixer_module('fix_macosx_stack')
+        stack_fixer_function = stack_fixer_module.fixSymbols
+    elif mozinfo.isLinux:
+        stack_fixer_module = import_stack_fixer_module('fix_linux_stack')
+        stack_fixer_function = stack_fixer_module.fixSymbols
+
+    return stack_fixer_function
+
+
 """ Control-C handling """
 gotSIGINT = False
 def markGotSIGINT(signum, stackFrame):
     global gotSIGINT
     gotSIGINT = True
 
 class XPCShellTestThread(Thread):
     def __init__(self, test_object, event, cleanup_dir_list, retry=True,
@@ -121,16 +155,17 @@ class XPCShellTestThread(Thread):
         self.profileName = kwargs.get('profileName')
         self.singleFile = kwargs.get('singleFile')
         self.env = copy.deepcopy(kwargs.get('env'))
         self.symbolsPath = kwargs.get('symbolsPath')
         self.logfiles = kwargs.get('logfiles')
         self.xpcshell = kwargs.get('xpcshell')
         self.xpcsRunArgs = kwargs.get('xpcsRunArgs')
         self.failureManifest = kwargs.get('failureManifest')
+        self.stack_fixer_function = kwargs.get('stack_fixer_function')
 
         self.tests_root_dir = tests_root_dir
         self.app_dir_key = app_dir_key
         self.interactive = interactive
         self.verbose = verbose
         self.pStdout = pStdout
         self.pStderr = pStderr
         self.keep_going = keep_going
@@ -485,27 +520,33 @@ class XPCShellTestThread(Thread):
         """Parses process output for structured messages and saves output as it is
         read. Sets self.has_failure_output in case of evidence of a failure"""
         for line_string in output.splitlines():
             self.process_line(line_string)
 
         if self.saw_proc_start and not self.saw_proc_end:
             self.has_failure_output = True
 
+    def fix_text_output(self, line):
+        line = cleanup_encoding(line)
+        if self.stack_fixer_function is not None:
+            return self.stack_fixer_function(line)
+        return line
+
     def log_line(self, line):
         """Log a line of output (either a parser json object or text output from
         the test process"""
         if isinstance(line, basestring):
-            line = cleanup_encoding(line).rstrip("\r\n")
+            line = self.fix_text_output(line).rstrip('\r\n')
             self.log.process_output(self.proc_ident,
                                     line,
                                     command=self.complete_command)
         else:
             if 'message' in line:
-                line['message'] = cleanup_encoding(line['message'])
+                line['message'] = self.fix_text_output(line['message'])
             if 'xpcshell_process' in line:
                 line['thread'] =  ' '.join([current_thread().name, line['xpcshell_process']])
             else:
                 line['thread'] = current_thread().name
             self.log.log_raw(line)
 
     def log_full_output(self, output):
         """Log output any buffered output from the test process"""
@@ -1017,17 +1058,17 @@ class XPCShellTests(object):
                  manifest=None, testdirs=None, testPath=None, mobileArgs=None,
                  interactive=False, verbose=False, keepGoing=False, logfiles=True,
                  thisChunk=1, totalChunks=1, debugger=None,
                  debuggerArgs=None, debuggerInteractive=False,
                  profileName=None, mozInfo=None, sequential=False, shuffle=False,
                  testsRootDir=None, testingModulesDir=None, pluginsPath=None,
                  testClass=XPCShellTestThread, failureManifest=None,
                  log=None, stream=None, jsDebugger=False, jsDebuggerPort=0,
-                 test_tags=None, **otherOptions):
+                 test_tags=None, utility_path=None, **otherOptions):
         """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.
         |appPath|, if provided, is the path to an application directory.
         |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
@@ -1148,16 +1189,22 @@ class XPCShellTests(object):
         for k, v in self.mozInfo.items():
             if isinstance(k, unicode):
                 k = k.encode('ascii')
             fixedInfo[k] = v
         self.mozInfo = fixedInfo
 
         mozinfo.update(self.mozInfo)
 
+        self.stack_fixer_function = None
+        if utility_path and os.path.exists(utility_path):
+            self.stack_fixer_function = find_stack_fixer(mozinfo,
+                                                         utility_path,
+                                                         self.symbolsPath)
+
         # buildEnvironment() needs mozInfo, so we call it after mozInfo is initialized.
         self.buildEnvironment()
 
         # The appDirKey is a optional entry in either the default or individual test
         # sections that defines a relative application directory for test runs. If
         # defined we pass 'grePath/$appDirKey' for the -a parameter of the xpcshell
         # test harness.
         appDirKey = None
@@ -1195,16 +1242,17 @@ class XPCShellTests(object):
             'singleFile': self.singleFile,
             'env': self.env, # making a copy of this in the testthreads
             'symbolsPath': self.symbolsPath,
             'logfiles': self.logfiles,
             'xpcshell': self.xpcshell,
             'xpcsRunArgs': self.xpcsRunArgs,
             'failureManifest': failureManifest,
             'harness_timeout': self.harness_timeout,
+            'stack_fixer_function': self.stack_fixer_function,
         }
 
         if self.sequential:
             # Allow user to kill hung xpcshell subprocess with SIGINT
             # when we are only running tests sequentially.
             signal.signal(signal.SIGINT, markGotSIGINT)
 
         if self.debuggerInfo:
@@ -1481,16 +1529,21 @@ class XPCShellOptions(OptionParser):
                         help="The port to listen on for a debugger connection if "
                              "--jsdebugger is specified.")
         self.add_option("--tag",
                         action="append", dest="test_tags",
                         default=None,
                         help="filter out tests that don't have the given tag. Can be "
                              "used multiple times in which case the test must contain "
                              "at least one of the given tags.")
+        self.add_option("--utility-path",
+                        action="store", dest="utility_path",
+                        default=None,
+                        help="Path to a directory containing utility programs, such "
+                             "as stack fixer scripts.")
 
 def main():
     parser = XPCShellOptions()
     structured.commandline.add_logging_group(parser)
     options, args = parser.parse_args()
 
 
     log = structured.commandline.setup_logging("XPCShell",
--- a/testing/xpcshell/selftest.py
+++ b/testing/xpcshell/selftest.py
@@ -352,16 +352,17 @@ add_task(function test_2() {
 
 class XPCShellTestsTests(unittest.TestCase):
     """
     Yes, these are unit tests for a unit test harness.
     """
     def setUp(self):
         self.log = StringIO()
         self.tempdir = tempfile.mkdtemp()
+        self.utility_path = os.path.join(objdir, 'dist', 'bin')
         logger = structured.commandline.setup_logging("selftest%s" % id(self),
                                                       {},
                                                       {"tbpl": self.log})
         self.x = XPCShellTests(logger)
         self.x.harness_timeout = 15
 
     def tearDown(self):
         shutil.rmtree(self.tempdir)
@@ -404,17 +405,18 @@ tail =
         """
         self.assertEquals(expected,
                           self.x.runTests(xpcshellBin,
                                           manifest=self.manifest,
                                           mozInfo=mozinfo.info,
                                           shuffle=shuffle,
                                           testsRootDir=self.tempdir,
                                           verbose=verbose,
-                                          sequential=True),
+                                          sequential=True,
+                                          utility_path=self.utility_path),
                           msg="""Tests should have %s, log:
 ========
 %s
 ========
 """ % ("passed" if expected else "failed", self.log.getvalue()))
 
     def _assertLog(self, s, expected):
         l = self.log.getvalue()