--- 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