Bug 1400380 - run Talos with browser in debug mode r?jmaher draft debugger
authorIonut Goldan <igoldan@mozilla.com>
Wed, 22 Nov 2017 15:49:27 +0200
changeset 707542 45b26ef4467ad76cdfbede23ba88cccbdd185a56
parent 707503 b4cef8d1dff06a1ec2b9bb17211c0c3c7f5b76fa
child 742973 71513286849242a5d2630d27553bd6f40488cba8
push id92149
push userbmo:igoldan@mozilla.com
push dateTue, 05 Dec 2017 14:02:23 +0000
reviewersjmaher
bugs1400380
milestone59.0a1
Bug 1400380 - run Talos with browser in debug mode r?jmaher MozReview-Commit-ID: JKQ7LPprXG1
testing/mozharness/mozharness/mozilla/testing/talos.py
testing/talos/talos/cmdline.py
testing/talos/talos/config.py
testing/talos/talos/ffsetup.py
testing/talos/talos/talos_process.py
testing/talos/talos/ttest.py
testing/talos/talos/utils.py
--- a/testing/mozharness/mozharness/mozilla/testing/talos.py
+++ b/testing/mozharness/mozharness/mozilla/testing/talos.py
@@ -9,16 +9,17 @@ run talos tests in a virtualenv
 """
 
 import os
 import sys
 import pprint
 import copy
 import re
 import shutil
+import subprocess
 import json
 
 import mozharness
 from mozharness.base.config import parse_config_file
 from mozharness.base.errors import PythonErrorList
 from mozharness.base.log import OutputParser, DEBUG, ERROR, CRITICAL
 from mozharness.base.log import INFO, WARNING
 from mozharness.base.python import Python3Virtualenv
@@ -696,18 +697,28 @@ class Talos(TestingMixin, MercurialScrip
             fname_pattern = '%s_%%s.log' % self.config['suite']
             mozlog_opts.append('--log-errorsummary=%s'
                                % os.path.join(env['MOZ_UPLOAD_DIR'],
                                               fname_pattern % 'errorsummary'))
             mozlog_opts.append('--log-raw=%s'
                                % os.path.join(env['MOZ_UPLOAD_DIR'],
                                               fname_pattern % 'raw'))
 
+        def launch_in_debug_mode(cmdline):
+            cmdline = set(cmdline)
+            debug_opts = {'--debug', '--debugger', '--debugger_args'}
+
+            return bool(debug_opts.intersection(cmdline))
+
         command = [python, run_tests] + options + mozlog_opts
-        self.return_code = self.run_command(command, cwd=self.workdir,
+        if launch_in_debug_mode(command):
+            talos_process = subprocess.Popen(command, cwd=self.workdir, env=env)
+            talos_process.wait()
+        else:
+            self.return_code = self.run_command(command, cwd=self.workdir,
                                             output_timeout=output_timeout,
                                             output_parser=parser,
                                             env=env)
         if parser.minidump_output:
             self.info("Looking at the minidump files for debugging purposes...")
             for item in parser.minidump_output:
                 self.run_command(["ls", "-l", item])
 
--- a/testing/talos/talos/cmdline.py
+++ b/testing/talos/talos/cmdline.py
@@ -166,15 +166,24 @@ def create_parser(mach_interface=False):
     add_arg('--disable-stylo', action="store_true",
             dest='disable_stylo',
             help='If given, disable Stylo via Environment variables.')
     add_arg('--stylo-threads', type=int,
             dest='stylothreads',
             help='If given, run Stylo with a certain number of threads')
     add_arg('--profile', type=str, default=None,
             help="Downloads a profile from TaskCluster and uses it")
+    debug_options = parser.add_argument_group('Command Arguments for debugging')
+    debug_options.add_argument('--debug', action='store_true',
+                               help='Enable the debugger. Not specifying a --debugger option will'
+                                    'result in the default debugger being used.')
+    debug_options.add_argument('--debugger', default=None,
+                               help='Name of debugger to use.')
+    debug_options.add_argument('--debugger-args', default=None, metavar='params',
+                               help='Command-line arguments to pass to the debugger itself; split'
+                                    'as the Bourne shell would.')
     add_logging_group(parser)
     return parser
 
 
 def parse_args(argv=None):
     parser = create_parser()
     return parser.parse_args(argv)
--- a/testing/talos/talos/config.py
+++ b/testing/talos/talos/config.py
@@ -412,16 +412,19 @@ def tests(config):
 
 
 def get_browser_config(config):
     required = ('preferences', 'extensions', 'browser_path', 'browser_wait',
                 'extra_args', 'buildid', 'env', 'init_url', 'webserver')
     optional = {'bcontroller_config': '${talos}/bcontroller.json',
                 'branch_name': '',
                 'child_process': 'plugin-container',
+                'debug': False,
+                'debugger': None,
+                'debugger_args': None,
                 'develop': False,
                 'process': '',
                 'framework': 'talos',
                 'repository': None,
                 'sourcestamp': None,
                 'symbols_path': None,
                 'test_timeout': 1200,
                 'xperf_path': None,
--- a/testing/talos/talos/ffsetup.py
+++ b/testing/talos/talos/ffsetup.py
@@ -13,17 +13,17 @@ import tempfile
 import mozfile
 import mozinfo
 import mozrunner
 from mozlog import get_proxy_logger
 from mozprocess import ProcessHandlerMixin
 from mozprofile.profile import Profile
 from talos import utils
 from talos.gecko_profile import GeckoProfile
-from talos.utils import TalosError
+from talos.utils import TalosError, run_in_debug_mode
 from talos import heavy
 
 LOG = get_proxy_logger()
 
 
 class FFSetup(object):
     """
     Initialize the browser environment before running a test.
@@ -51,16 +51,17 @@ class FFSetup(object):
     def __init__(self, browser_config, test_config):
         self.browser_config, self.test_config = browser_config, test_config
         self._tmp_dir = tempfile.mkdtemp()
         self.env = None
         # The profile dir must be named 'profile' because of xperf analysis
         # (in etlparser.py). TODO fix that ?
         self.profile_dir = os.path.join(self._tmp_dir, 'profile')
         self.gecko_profile = None
+        self.debug_mode = run_in_debug_mode(browser_config)
 
     def _init_env(self):
         self.env = dict(os.environ)
         for k, v in self.browser_config['env'].iteritems():
             self.env[k] = str(v)
         self.env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
         if self.browser_config['symbols_path']:
             self.env['MOZ_CRASHREPORTER'] = '1'
@@ -191,17 +192,18 @@ class FFSetup(object):
             self.gecko_profile.clean()
 
     def __enter__(self):
         LOG.info('Initialising browser for %s test...'
                  % self.test_config['name'])
         self._init_env()
         self._init_profile()
         try:
-            self._run_profile()
+            if not self.debug_mode:
+                self._run_profile()
         except:
             self.clean()
             raise
         self._init_gecko_profile()
         LOG.info('Browser initialized.')
         return self
 
     def __exit__(self, type, value, tb):
--- a/testing/talos/talos/talos_process.py
+++ b/testing/talos/talos/talos_process.py
@@ -1,16 +1,18 @@
 # 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/.
 from __future__ import absolute_import
 
 import pprint
+import signal
 import time
 import traceback
+import subprocess
 from threading import Event
 
 import mozcrash
 import psutil
 from mozlog import get_proxy_logger
 from mozprocess import ProcessHandler
 from utils import TalosError
 
@@ -73,17 +75,17 @@ class Reader(object):
 
         if not (line.startswith('JavaScript error:') or
                 line.startswith('JavaScript warning:')):
             LOG.process_output(self.proc.pid, line)
             self.output.append(line)
 
 
 def run_browser(command, minidump_dir, timeout=None, on_started=None,
-                **kwargs):
+                debug=None, debugger=None, debugger_args=None, **kwargs):
     """
     Run the browser using the given `command`.
 
     After the browser prints __endTimestamp, we give it 5
     seconds to quit and kill it if it's still alive at that point.
 
     Note that this method ensure that the process is killed at
     the end. If this is not possible, an exception will be raised.
@@ -97,30 +99,37 @@ def run_browser(command, minidump_dir, t
     :param on_started: a callback that can be used to do things just after
                        the browser has been started. The callback must takes
                        an argument, which is the psutil.Process instance
     :param kwargs: additional keyword arguments for the :class:`ProcessHandler`
                    instance
 
     Returns a ProcessContext instance, with available output and pid used.
     """
+
+    debugger_info = find_debugger_info(debug, debugger, debugger_args)
+    if debugger_info is not None:
+        return run_in_debug_mode(command, debugger_info,
+                                 on_started=on_started, env=kwargs.get('env'))
+
     context = ProcessContext()
     first_time = int(time.time()) * 1000
     wait_for_quit_timeout = 5
     event = Event()
     reader = Reader(event)
 
     LOG.info("Using env: %s" % pprint.pformat(kwargs['env']))
 
     kwargs['storeOutput'] = False
     kwargs['processOutputLine'] = reader
     kwargs['onFinish'] = event.set
     proc = ProcessHandler(command, **kwargs)
     reader.proc = proc
     proc.run()
+
     LOG.process_start(proc.pid, ' '.join(command))
     try:
         context.process = psutil.Process(proc.pid)
         if on_started:
             on_started(context.process)
         # wait until we saw __endTimestamp in the proc output,
         # or the browser just terminated - or we have a timeout
         if not event.wait(timeout):
@@ -160,8 +169,49 @@ def run_browser(command, minidump_dir, t
         % (int(time.time()) * 1000))
 
     if return_code is not None:
         LOG.process_exit(proc.pid, return_code)
     else:
         LOG.debug("Unable to detect exit code of the process %s." % proc.pid)
     context.output = reader.output
     return context
+
+
+def find_debugger_info(debug, debugger, debugger_args):
+    debuggerInfo = None
+    if debug or debugger or debugger_args:
+        import mozdebug
+
+        if not debugger:
+            # No debugger name was provided. Look for the default ones on
+            # current OS.
+            debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
+
+        debuggerInfo = None
+        if debugger:
+            debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
+
+        if debuggerInfo is None:
+            raise TalosError('Could not find a suitable debugger in your PATH.')
+
+    return debuggerInfo
+
+
+def run_in_debug_mode(command, debugger_info, on_started=None, env=None):
+    signal.signal(signal.SIGINT, lambda sigid, frame: None)
+    context = ProcessContext()
+    command_under_dbg = [debugger_info.path] + debugger_info.args + command
+
+    ttest_process = subprocess.Popen(command_under_dbg, env=env)
+
+    context.process = psutil.Process(ttest_process.pid)
+    if on_started:
+        on_started(context.process)
+
+    return_code = ttest_process.wait()
+
+    if return_code is not None:
+        LOG.process_exit(ttest_process.pid, return_code)
+    else:
+        LOG.debug("Unable to detect exit code of the process %s." % ttest_process.pid)
+
+    return context
--- a/testing/talos/talos/ttest.py
+++ b/testing/talos/talos/ttest.py
@@ -24,31 +24,31 @@ import mozcrash
 import mozfile
 import results
 import talosconfig
 import utils
 from mozlog import get_proxy_logger
 from talos.cmanager import CounterManagement
 from talos.ffsetup import FFSetup
 from talos.talos_process import run_browser
-from talos.utils import TalosCrash, TalosError, TalosRegression
+from talos.utils import TalosCrash, TalosError, TalosRegression, run_in_debug_mode
 
 LOG = get_proxy_logger()
 
 
 class TTest(object):
     def check_for_crashes(self, browser_config, minidump_dir, test_name):
         # check for minidumps
         found = mozcrash.check_for_crashes(minidump_dir,
                                            browser_config['symbols_path'],
                                            test_name=test_name)
         mozfile.remove(minidump_dir)
 
         if found:
-            raise TalosCrash("Found crashes after test run, terminating test")
+            raise TalosCrash('Found crashes after test run, terminating test')
 
     def runTest(self, browser_config, test_config):
         """
             Runs an url based test on the browser as specified in the
             browser_config dictionary
 
         Args:
             browser_config:  Dictionary of configuration options for the
@@ -58,51 +58,51 @@ class TTest(object):
 
         """
 
         with FFSetup(browser_config, test_config) as setup:
             return self._runTest(browser_config, test_config, setup)
 
     @staticmethod
     def _get_counter_prefix():
-        if platform.system() == "Linux":
+        if platform.system() == 'Linux':
             return 'linux'
-        elif platform.system() in ("Windows", "Microsoft"):
+        elif platform.system() in ('Windows', 'Microsoft'):
             if '6.1' in platform.version():  # w7
                 return 'w7'
             elif '6.2' in platform.version():  # w8
                 return 'w8'
             # Bug 1264325 - FIXME: with python 2.7.11: reports win8 instead of 8.1
             elif '6.3' in platform.version():
                 return 'w8'
             # Bug 1264325 - FIXME: with python 2.7.11: reports win8 instead of 10
             elif '10.0' in platform.version():
                 return 'w8'
             else:
                 raise TalosError('unsupported windows version')
-        elif platform.system() == "Darwin":
+        elif platform.system() == 'Darwin':
             return 'mac'
 
     def _runTest(self, browser_config, test_config, setup):
         minidump_dir = os.path.join(setup.profile_dir, 'minidumps')
         counters = test_config.get('%s_counters' % self._get_counter_prefix(), [])
         resolution = test_config['resolution']
 
         # add the mainthread_io to the environment variable, as defined
         # in test.py configs
         here = os.path.dirname(os.path.realpath(__file__))
         if test_config['mainthread']:
-            mainthread_io = os.path.join(here, "mainthread_io.log")
+            mainthread_io = os.path.join(here, 'mainthread_io.log')
             setup.env['MOZ_MAIN_THREAD_IO_LOG'] = mainthread_io
 
         if browser_config['disable_stylo']:
             if browser_config['stylothreads']:
-                raise TalosError("--disable-stylo conflicts with --stylo-threads")
+                raise TalosError('--disable-stylo conflicts with --stylo-threads')
             if browser_config['enable_stylo']:
-                raise TalosError("--disable-stylo conflicts with --enable-stylo")
+                raise TalosError('--disable-stylo conflicts with --enable-stylo')
 
         # As we transition to Stylo, we need to set env vars and output data properly
         if browser_config['enable_stylo']:
             setup.env['STYLO_FORCE_ENABLED'] = '1'
         if browser_config['disable_stylo']:
             setup.env['STYLO_FORCE_DISABLED'] = '1'
 
         # During the Stylo transition, measure different number of threads
@@ -119,68 +119,68 @@ class TTest(object):
 
         # setup global (cross-cycle) responsiveness counters
         global_counters = {}
         if browser_config.get('xperf_path'):
             for c in test_config.get('xperf_counters', []):
                 global_counters[c] = []
 
         if test_config.get('responsiveness') and \
-           platform.system() != "Darwin":
+           platform.system() != 'Darwin':
             # ignore osx for now as per bug 1245793
             setup.env['MOZ_INSTRUMENT_EVENT_LOOP'] = '1'
             setup.env['MOZ_INSTRUMENT_EVENT_LOOP_THRESHOLD'] = '20'
             setup.env['MOZ_INSTRUMENT_EVENT_LOOP_INTERVAL'] = '10'
             global_counters['responsiveness'] = []
 
         setup.env['JSGC_DISABLE_POISONING'] = '1'
         setup.env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
 
         # if using mitmproxy we must allow access to 'external' sites
         if browser_config.get('mitmproxy', False):
-            LOG.info("Using mitmproxy so setting MOZ_DISABLE_NONLOCAL_CONNECTIONS to 0")
+            LOG.info('Using mitmproxy so setting MOZ_DISABLE_NONLOCAL_CONNECTIONS to 0')
             setup.env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '0'
 
         # instantiate an object to hold test results
         test_results = results.TestResults(
             test_config,
             global_counters,
             browser_config.get('framework')
         )
 
         for i in range(test_config['cycles']):
-            LOG.info("Running cycle %d/%d for %s test..."
+            LOG.info('Running cycle %d/%d for %s test...'
                      % (i+1, test_config['cycles'], test_config['name']))
 
             # remove the browser  error file
             mozfile.remove(browser_config['error_filename'])
 
             # reinstall any file whose stability we need to ensure across
             # the cycles
             if test_config.get('reinstall', ''):
                 for keep in test_config['reinstall']:
                     origin = os.path.join(test_config['profile_path'],
                                           keep)
                     dest = os.path.join(setup.profile_dir, keep)
-                    LOG.debug("Reinstalling %s on top of %s"
+                    LOG.debug('Reinstalling %s on top of %s'
                               % (origin, dest))
                     shutil.copy(origin, dest)
 
             # Run the test
             timeout = test_config.get('timeout', 7200)  # 2 hours default
             if setup.gecko_profile:
                 # When profiling, give the browser some extra time
                 # to dump the profile.
                 timeout += 5 * 60
                 # store profiling info for pageloader; too late to add it as browser pref
                 setup.env["TPPROFILINGINFO"] = json.dumps(setup.gecko_profile.profiling_info)
 
             command_args = utils.GenerateBrowserCommandLine(
-                browser_config["browser_path"],
-                browser_config["extra_args"],
+                browser_config['browser_path'],
+                browser_config['extra_args'],
                 setup.profile_dir,
                 test_config['url'],
                 profiling_info=(setup.gecko_profile.profiling_info
                                 if setup.gecko_profile else None)
             )
 
             mainthread_error_count = 0
             if test_config['setup']:
@@ -204,41 +204,44 @@ class TTest(object):
                 pcontext = run_browser(
                     command_args,
                     minidump_dir,
                     timeout=timeout,
                     env=setup.env,
                     # start collecting counters as soon as possible
                     on_started=(counter_management.start
                                 if counter_management else None),
+                    debug=browser_config['debug'],
+                    debugger=browser_config['debugger'],
+                    debugger_args=browser_config['debugger_args']
                 )
             except:
                 self.check_for_crashes(browser_config, minidump_dir,
                                        test_config['name'])
                 raise
             finally:
                 if counter_management:
                     counter_management.stop()
 
             if test_config['mainthread']:
-                rawlog = os.path.join(here, "mainthread_io.log")
+                rawlog = os.path.join(here, 'mainthread_io.log')
                 if os.path.exists(rawlog):
                     processedlog = \
                         os.path.join(here, 'mainthread_io.json')
                     xre_path = \
                         os.path.dirname(browser_config['browser_path'])
                     mtio_py = os.path.join(here, 'mainthreadio.py')
                     command = ['python', mtio_py, rawlog,
                                processedlog, xre_path]
                     mtio = subprocess.Popen(command,
                                             env=os.environ.copy(),
                                             stdout=subprocess.PIPE)
                     output, stderr = mtio.communicate()
                     for line in output.split('\n'):
-                        if line.strip() == "":
+                        if line.strip() == '':
                             continue
 
                         print(line)
                         mainthread_error_count += 1
                     mozfile.remove(rawlog)
 
             if test_config['cleanup']:
                 # HACK: add the pid to support xperf where we require
@@ -256,36 +259,37 @@ class TTest(object):
             for fname in ('sessionstore.js', '.parentlock',
                           'sessionstore.bak'):
                 mozfile.remove(os.path.join(setup.profile_dir, fname))
 
             # check for xperf errors
             if os.path.exists(browser_config['error_filename']) or \
                mainthread_error_count > 0:
                 raise TalosRegression(
-                    "Talos has found a regression, if you have questions"
-                    " ask for help in irc on #perf"
+                    'Talos has found a regression, if you have questions'
+                    ' ask for help in irc on #perf'
                 )
 
             # add the results from the browser output
-            test_results.add(
-                '\n'.join(pcontext.output),
-                counter_results=(counter_management.results()
-                                 if counter_management
-                                 else None)
-            )
+            if not run_in_debug_mode(browser_config):
+                test_results.add(
+                    '\n'.join(pcontext.output),
+                    counter_results=(counter_management.results()
+                                     if counter_management
+                                     else None)
+                )
 
             if setup.gecko_profile:
                 setup.gecko_profile.symbolicate(i)
 
             self.check_for_crashes(browser_config, minidump_dir,
                                    test_config['name'])
 
         # include global (cross-cycle) counters
         test_results.all_counter_results.extend(
             [{key: value} for key, value in global_counters.items()]
         )
         for c in test_results.all_counter_results:
             for key, value in c.items():
-                LOG.debug("COUNTER %r: %s" % (key, value))
+                LOG.debug('COUNTER %r: %s' % (key, value))
 
         # return results
         return test_results
--- a/testing/talos/talos/utils.py
+++ b/testing/talos/talos/utils.py
@@ -25,17 +25,17 @@ class Timer(object):
         self._start_time = 0
         self.start()
 
     def start(self):
         self._start_time = time.time()
 
     def elapsed(self):
         seconds = time.time() - self._start_time
-        return time.strftime("%H:%M:%S", time.gmtime(seconds))
+        return time.strftime('%H:%M:%S', time.gmtime(seconds))
 
 
 class TalosError(Exception):
     "Errors found while running the talos harness."
 
 
 class TalosRegression(Exception):
     """When a regression is detected at runtime, report it properly
@@ -112,21 +112,21 @@ def urlsplit(url, default_scheme='file')
 
 
 def parse_pref(value):
     """parse a preference value from a string"""
     from mozprofile.prefs import Preferences
     return Preferences.cast(value)
 
 
-def GenerateBrowserCommandLine(browser_path, extra_args, profile_dir,
-                               url, profiling_info=None):
+def GenerateBrowserCommandLine(browser_path, extra_args, profile_dir, url, profiling_info=None):
     # TODO: allow for spaces in file names on Windows
     command_args = [browser_path.strip()]
-    if platform.system() == "Darwin":
+
+    if platform.system() == 'Darwin':
         command_args.extend(['-foreground'])
 
     if isinstance(extra_args, list):
         command_args.extend(extra_args)
 
     elif extra_args.strip():
         command_args.extend([extra_args])
 
@@ -157,8 +157,15 @@ def indexed_items(itr):
     Generator that allows us to figure out which item is the last one so
     that we can serialize this data properly
     """
     prev_i, prev_val = 0, itr.next()
     for i, val in enumerate(itr, start=1):
         yield prev_i, prev_val
         prev_i, prev_val = i, val
     yield -1, prev_val
+
+
+def run_in_debug_mode(browser_config):
+    if browser_config.get('debug') or browser_config.get('debugger') or \
+            browser_config.get('debugg_args'):
+        return True
+    return False