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 267923 98e985a2d950eef472bc67a7555237f43f326d66
parent 267922 182ac56399c47c0b5df657596db53064b107594d
child 267924 aca8404dc72cc801ef17589d717c74bff0d48333
push id8157
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:36:23 +0000
treeherdermozilla-aurora@d480e05bd276 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersted
bugs487494
milestone41.0a1
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()