Index: infra/scripts/legacy/scripts/slave/runtest.py
|
diff --git a/infra/scripts/legacy/scripts/slave/runtest.py b/infra/scripts/legacy/scripts/slave/runtest.py
|
new file mode 100755
|
index 0000000000000000000000000000000000000000..75c3366a2b37976180611265a21596f00bfed51a
|
--- /dev/null
|
+++ b/infra/scripts/legacy/scripts/slave/runtest.py
|
@@ -0,0 +1,1948 @@
|
+#!/usr/bin/env python
|
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
+# Use of this source code is governed by a BSD-style license that can be
|
+# found in the LICENSE file.
|
+
|
+"""A tool used to run a Chrome test executable and process the output.
|
+
|
+This script is used by the buildbot slaves. It must be run from the outer
|
+build directory, e.g. chrome-release/build/.
|
+
|
+For a list of command-line options, call this script with '--help'.
|
+"""
|
+
|
+import ast
|
+import copy
|
+import datetime
|
+import exceptions
|
+import gzip
|
+import hashlib
|
+import json
|
+import logging
|
+import optparse
|
+import os
|
+import re
|
+import stat
|
+import subprocess
|
+import sys
|
+import tempfile
|
+
|
+# The following note was added in 2010 by nsylvain:
|
+#
|
+# sys.path needs to be modified here because python2.6 automatically adds the
|
+# system "google" module (/usr/lib/pymodules/python2.6/google) to sys.modules
|
+# when we import "chromium_config" (I don't know why it does this). This causes
|
+# the import of our local "google.*" modules to fail because python seems to
|
+# only look for a system "google.*", even if our path is in sys.path before
|
+# importing "google.*". If we modify sys.path here, before importing
|
+# "chromium_config", python2.6 properly uses our path to find our "google.*"
|
+# (even though it still automatically adds the system "google" module to
|
+# sys.modules, and probably should still be using that to resolve "google.*",
|
+# which I really don't understand).
|
+sys.path.insert(0, os.path.abspath('src/tools/python'))
|
+
|
+from common import chromium_utils
|
+from common import gtest_utils
|
+
|
+# TODO(crbug.com/403564). We almost certainly shouldn't be importing this.
|
+import config
|
+
|
+from slave import annotation_utils
|
+from slave import build_directory
|
+from slave import crash_utils
|
+from slave import gtest_slave_utils
|
+from slave import performance_log_processor
|
+from slave import results_dashboard
|
+from slave import slave_utils
|
+from slave import telemetry_utils
|
+from slave import xvfb
|
+
|
+USAGE = '%s [options] test.exe [test args]' % os.path.basename(sys.argv[0])
|
+
|
+CHROME_SANDBOX_PATH = '/opt/chromium/chrome_sandbox'
|
+
|
+# Directory to write JSON for test results into.
|
+DEST_DIR = 'gtest_results'
|
+
|
+# Names of httpd configuration file under different platforms.
|
+HTTPD_CONF = {
|
+ 'linux': 'httpd2_linux.conf',
|
+ 'mac': 'httpd2_mac.conf',
|
+ 'win': 'httpd.conf'
|
+}
|
+# Regex matching git comment lines containing svn revision info.
|
+GIT_SVN_ID_RE = re.compile(r'^git-svn-id: .*@([0-9]+) .*$')
|
+# Regex for the master branch commit position.
|
+GIT_CR_POS_RE = re.compile(r'^Cr-Commit-Position: refs/heads/master@{#(\d+)}$')
|
+
|
+# The directory that this script is in.
|
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
+
|
+LOG_PROCESSOR_CLASSES = {
|
+ 'gtest': gtest_utils.GTestLogParser,
|
+ 'graphing': performance_log_processor.GraphingLogProcessor,
|
+ 'pagecycler': performance_log_processor.GraphingPageCyclerLogProcessor,
|
+}
|
+
|
+
|
+def _GetTempCount():
|
+ """Returns the number of files and directories inside the temporary dir."""
|
+ return len(os.listdir(tempfile.gettempdir()))
|
+
|
+
|
+def _LaunchDBus():
|
+ """Launches DBus to work around a bug in GLib.
|
+
|
+ Works around a bug in GLib where it performs operations which aren't
|
+ async-signal-safe (in particular, memory allocations) between fork and exec
|
+ when it spawns subprocesses. This causes threads inside Chrome's browser and
|
+ utility processes to get stuck, and this harness to hang waiting for those
|
+ processes, which will never terminate. This doesn't happen on users'
|
+ machines, because they have an active desktop session and the
|
+ DBUS_SESSION_BUS_ADDRESS environment variable set, but it does happen on the
|
+ bots. See crbug.com/309093 for more details.
|
+
|
+ Returns:
|
+ True if it actually spawned DBus.
|
+ """
|
+ import platform
|
+ if (platform.uname()[0].lower() == 'linux' and
|
+ 'DBUS_SESSION_BUS_ADDRESS' not in os.environ):
|
+ try:
|
+ print 'DBUS_SESSION_BUS_ADDRESS env var not found, starting dbus-launch'
|
+ dbus_output = subprocess.check_output(['dbus-launch']).split('\n')
|
+ for line in dbus_output:
|
+ m = re.match(r'([^=]+)\=(.+)', line)
|
+ if m:
|
+ os.environ[m.group(1)] = m.group(2)
|
+ print ' setting %s to %s' % (m.group(1), m.group(2))
|
+ return True
|
+ except (subprocess.CalledProcessError, OSError) as e:
|
+ print 'Exception while running dbus_launch: %s' % e
|
+ return False
|
+
|
+
|
+def _ShutdownDBus():
|
+ """Manually kills the previously-launched DBus daemon.
|
+
|
+ It appears that passing --exit-with-session to dbus-launch in
|
+ _LaunchDBus(), above, doesn't cause the launched dbus-daemon to shut
|
+ down properly. Manually kill the sub-process using the PID it gave
|
+ us at launch time.
|
+
|
+ This function is called when the flag --spawn-dbus is given, and if
|
+ _LaunchDBus(), above, actually spawned the dbus-daemon.
|
+ """
|
+ import signal
|
+ if 'DBUS_SESSION_BUS_PID' in os.environ:
|
+ dbus_pid = os.environ['DBUS_SESSION_BUS_PID']
|
+ try:
|
+ os.kill(int(dbus_pid), signal.SIGTERM)
|
+ print ' killed dbus-daemon with PID %s' % dbus_pid
|
+ except OSError as e:
|
+ print ' error killing dbus-daemon with PID %s: %s' % (dbus_pid, e)
|
+ # Try to clean up any stray DBUS_SESSION_BUS_ADDRESS environment
|
+ # variable too. Some of the bots seem to re-invoke runtest.py in a
|
+ # way that this variable sticks around from run to run.
|
+ if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
|
+ del os.environ['DBUS_SESSION_BUS_ADDRESS']
|
+ print ' cleared DBUS_SESSION_BUS_ADDRESS environment variable'
|
+
|
+
|
+def _RunGTestCommand(
|
+ options, command, extra_env, log_processor=None, pipes=None):
|
+ """Runs a test, printing and possibly processing the output.
|
+
|
+ Args:
|
+ options: Options passed for this invocation of runtest.py.
|
+ command: A list of strings in a command (the command and its arguments).
|
+ extra_env: A dictionary of extra environment variables to set.
|
+ log_processor: A log processor instance which has the ProcessLine method.
|
+ pipes: A list of command string lists which the output will be piped to.
|
+
|
+ Returns:
|
+ The process return code.
|
+ """
|
+ env = os.environ.copy()
|
+ if extra_env:
|
+ print 'Additional test environment:'
|
+ for k, v in sorted(extra_env.items()):
|
+ print ' %s=%s' % (k, v)
|
+ env.update(extra_env or {})
|
+
|
+ # Trigger bot mode (test retries, redirection of stdio, possibly faster,
|
+ # etc.) - using an environment variable instead of command-line flags because
|
+ # some internal waterfalls run this (_RunGTestCommand) for totally non-gtest
|
+ # code.
|
+ # TODO(phajdan.jr): Clean this up when internal waterfalls are fixed.
|
+ env.update({'CHROMIUM_TEST_LAUNCHER_BOT_MODE': '1'})
|
+
|
+ log_processors = {}
|
+ if log_processor:
|
+ log_processors[log_processor.__class__.__name__] = log_processor
|
+
|
+ if (not 'GTestLogParser' in log_processors and
|
+ options.log_processor_output_file):
|
+ log_processors['GTestLogParser'] = gtest_utils.GTestLogParser()
|
+
|
+ def _ProcessLine(line):
|
+ for current_log_processor in log_processors.values():
|
+ current_log_processor.ProcessLine(line)
|
+
|
+ result = chromium_utils.RunCommand(
|
+ command, pipes=pipes, parser_func=_ProcessLine, env=env)
|
+
|
+ if options.log_processor_output_file:
|
+ _WriteLogProcessorResultsToOutput(
|
+ log_processors['GTestLogParser'], options.log_processor_output_file)
|
+
|
+ return result
|
+
|
+
|
+def _GetMaster():
|
+ """Return the master name for the current host."""
|
+ return chromium_utils.GetActiveMaster()
|
+
|
+
|
+def _GetMasterString(master):
|
+ """Returns a message describing what the master is."""
|
+ return '[Running for master: "%s"]' % master
|
+
|
+
|
+def _GetGitCommitPositionFromLog(log):
|
+ """Returns either the commit position or svn rev from a git log."""
|
+ # Parse from the bottom up, in case the commit message embeds the message
|
+ # from a different commit (e.g., for a revert).
|
+ for r in [GIT_CR_POS_RE, GIT_SVN_ID_RE]:
|
+ for line in reversed(log.splitlines()):
|
+ m = r.match(line.strip())
|
+ if m:
|
+ return m.group(1)
|
+ return None
|
+
|
+
|
+def _GetGitCommitPosition(dir_path):
|
+ """Extracts the commit position or svn revision number of the HEAD commit."""
|
+ git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
|
+ p = subprocess.Popen(
|
+ [git_exe, 'log', '-n', '1', '--pretty=format:%B', 'HEAD'],
|
+ cwd=dir_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
+ (log, _) = p.communicate()
|
+ if p.returncode != 0:
|
+ return None
|
+ return _GetGitCommitPositionFromLog(log)
|
+
|
+
|
+def _IsGitDirectory(dir_path):
|
+ """Checks whether the given directory is in a git repository.
|
+
|
+ Args:
|
+ dir_path: The directory path to be tested.
|
+
|
+ Returns:
|
+ True if given directory is in a git repository, False otherwise.
|
+ """
|
+ git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
|
+ with open(os.devnull, 'w') as devnull:
|
+ p = subprocess.Popen([git_exe, 'rev-parse', '--git-dir'],
|
+ cwd=dir_path, stdout=devnull, stderr=devnull)
|
+ return p.wait() == 0
|
+
|
+
|
+def _GetRevision(in_directory):
|
+ """Returns the SVN revision, git commit position, or git hash.
|
+
|
+ Args:
|
+ in_directory: A directory in the repository to be checked.
|
+
|
+ Returns:
|
+ An SVN revision as a string if the given directory is in a SVN repository,
|
+ or a git commit position number, or if that's not available, a git hash.
|
+ If all of that fails, an empty string is returned.
|
+ """
|
+ import xml.dom.minidom
|
+ if not os.path.exists(os.path.join(in_directory, '.svn')):
|
+ if _IsGitDirectory(in_directory):
|
+ svn_rev = _GetGitCommitPosition(in_directory)
|
+ if svn_rev:
|
+ return svn_rev
|
+ return _GetGitRevision(in_directory)
|
+ else:
|
+ return ''
|
+
|
+ # Note: Not thread safe: http://bugs.python.org/issue2320
|
+ output = subprocess.Popen(['svn', 'info', '--xml'],
|
+ cwd=in_directory,
|
+ shell=(sys.platform == 'win32'),
|
+ stdout=subprocess.PIPE).communicate()[0]
|
+ try:
|
+ dom = xml.dom.minidom.parseString(output)
|
+ return dom.getElementsByTagName('entry')[0].getAttribute('revision')
|
+ except xml.parsers.expat.ExpatError:
|
+ return ''
|
+ return ''
|
+
|
+
|
+def _GetGitRevision(in_directory):
|
+ """Returns the git hash tag for the given directory.
|
+
|
+ Args:
|
+ in_directory: The directory where git is to be run.
|
+
|
+ Returns:
|
+ The git SHA1 hash string.
|
+ """
|
+ git_exe = 'git.bat' if sys.platform.startswith('win') else 'git'
|
+ p = subprocess.Popen(
|
+ [git_exe, 'rev-parse', 'HEAD'],
|
+ cwd=in_directory, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
+ (stdout, _) = p.communicate()
|
+ return stdout.strip()
|
+
|
+
|
+def _GenerateJSONForTestResults(options, log_processor):
|
+ """Generates or updates a JSON file from the gtest results XML and upload the
|
+ file to the archive server.
|
+
|
+ The archived JSON file will be placed at:
|
+ www-dir/DEST_DIR/buildname/testname/results.json
|
+ on the archive server. NOTE: This will be deprecated.
|
+
|
+ Args:
|
+ options: command-line options that are supposed to have build_dir,
|
+ results_directory, builder_name, build_name and test_output_xml values.
|
+ log_processor: An instance of PerformanceLogProcessor or similar class.
|
+
|
+ Returns:
|
+ True upon success, False upon failure.
|
+ """
|
+ results_map = None
|
+ try:
|
+ if (os.path.exists(options.test_output_xml) and
|
+ not _UsingGtestJson(options)):
|
+ results_map = gtest_slave_utils.GetResultsMapFromXML(
|
+ options.test_output_xml)
|
+ else:
|
+ if _UsingGtestJson(options):
|
+ sys.stderr.write('using JSON summary output instead of gtest XML\n')
|
+ else:
|
+ sys.stderr.write(
|
+ ('"%s" \\ "%s" doesn\'t exist: Unable to generate JSON from XML, '
|
+ 'using log output.\n') % (os.getcwd(), options.test_output_xml))
|
+ # The file did not get generated. See if we can generate a results map
|
+ # from the log output.
|
+ results_map = gtest_slave_utils.GetResultsMap(log_processor)
|
+ except Exception as e:
|
+ # This error will be caught by the following 'not results_map' statement.
|
+ print 'Error: ', e
|
+
|
+ if not results_map:
|
+ print 'No data was available to update the JSON results'
|
+ # Consider this non-fatal.
|
+ return True
|
+
|
+ build_dir = os.path.abspath(options.build_dir)
|
+ slave_name = options.builder_name or slave_utils.SlaveBuildName(build_dir)
|
+
|
+ generate_json_options = copy.copy(options)
|
+ generate_json_options.build_name = slave_name
|
+ generate_json_options.input_results_xml = options.test_output_xml
|
+ generate_json_options.builder_base_url = '%s/%s/%s/%s' % (
|
+ config.Master.archive_url, DEST_DIR, slave_name, options.test_type)
|
+ generate_json_options.master_name = options.master_class_name or _GetMaster()
|
+ generate_json_options.test_results_server = config.Master.test_results_server
|
+
|
+ print _GetMasterString(generate_json_options.master_name)
|
+
|
+ generator = None
|
+
|
+ try:
|
+ if options.revision:
|
+ generate_json_options.chrome_revision = options.revision
|
+ else:
|
+ chrome_dir = chromium_utils.FindUpwardParent(build_dir, 'third_party')
|
+ generate_json_options.chrome_revision = _GetRevision(chrome_dir)
|
+
|
+ if options.webkit_revision:
|
+ generate_json_options.webkit_revision = options.webkit_revision
|
+ else:
|
+ webkit_dir = chromium_utils.FindUpward(
|
+ build_dir, 'third_party', 'WebKit', 'Source')
|
+ generate_json_options.webkit_revision = _GetRevision(webkit_dir)
|
+
|
+ # Generate results JSON file and upload it to the appspot server.
|
+ generator = gtest_slave_utils.GenerateJSONResults(
|
+ results_map, generate_json_options)
|
+
|
+ except Exception as e:
|
+ print 'Unexpected error while generating JSON: %s' % e
|
+ sys.excepthook(*sys.exc_info())
|
+ return False
|
+
|
+ # The code can throw all sorts of exceptions, including
|
+ # slave.gtest.networktransaction.NetworkTimeout so just trap everything.
|
+ # Earlier versions of this code ignored network errors, so until a
|
+ # retry mechanism is added, continue to do so rather than reporting
|
+ # an error.
|
+ try:
|
+ # Upload results JSON file to the appspot server.
|
+ gtest_slave_utils.UploadJSONResults(generator)
|
+ except Exception as e:
|
+ # Consider this non-fatal for the moment.
|
+ print 'Unexpected error while uploading JSON: %s' % e
|
+ sys.excepthook(*sys.exc_info())
|
+
|
+ return True
|
+
|
+
|
+def _BuildTestBinaryCommand(_build_dir, test_exe_path, options):
|
+ """Builds a command to run a test binary.
|
+
|
+ Args:
|
+ build_dir: Path to the tools/build directory.
|
+ test_exe_path: Path to test command binary.
|
+ options: Options passed for this invocation of runtest.py.
|
+
|
+ Returns:
|
+ A command, represented as a list of command parts.
|
+ """
|
+ command = [
|
+ test_exe_path,
|
+ ]
|
+
|
+ if options.annotate == 'gtest':
|
+ command.append('--test-launcher-bot-mode')
|
+
|
+ if options.total_shards and options.shard_index:
|
+ command.extend([
|
+ '--test-launcher-total-shards=%d' % options.total_shards,
|
+ '--test-launcher-shard-index=%d' % (options.shard_index - 1)])
|
+
|
+ return command
|
+
|
+
|
+def _UsingGtestJson(options):
|
+ """Returns True if we're using GTest JSON summary."""
|
+ return (options.annotate == 'gtest' and
|
+ not options.run_python_script and
|
+ not options.run_shell_script)
|
+
|
+
|
+def _ListLogProcessors(selection):
|
+ """Prints a list of available log processor classes iff the input is 'list'.
|
+
|
+ Args:
|
+ selection: A log processor name, or the string "list".
|
+
|
+ Returns:
|
+ True if a list was printed, False otherwise.
|
+ """
|
+ shouldlist = selection and selection == 'list'
|
+ if shouldlist:
|
+ print
|
+ print 'Available log processors:'
|
+ for p in LOG_PROCESSOR_CLASSES:
|
+ print ' ', p, LOG_PROCESSOR_CLASSES[p].__name__
|
+
|
+ return shouldlist
|
+
|
+
|
+def _SelectLogProcessor(options, is_telemetry):
|
+ """Returns a log processor class based on the command line options.
|
+
|
+ Args:
|
+ options: Command-line options (from OptionParser).
|
+ is_telemetry: bool for whether to create a telemetry log processor.
|
+
|
+ Returns:
|
+ A log processor class, or None.
|
+ """
|
+ if _UsingGtestJson(options):
|
+ return gtest_utils.GTestJSONParser
|
+
|
+ if is_telemetry:
|
+ return telemetry_utils.TelemetryResultsProcessor
|
+
|
+ if options.annotate:
|
+ if options.annotate in LOG_PROCESSOR_CLASSES:
|
+ if options.generate_json_file and options.annotate != 'gtest':
|
+ raise NotImplementedError('"%s" doesn\'t make sense with '
|
+ 'options.generate_json_file.')
|
+ else:
|
+ return LOG_PROCESSOR_CLASSES[options.annotate]
|
+ else:
|
+ raise KeyError('"%s" is not a valid GTest parser!' % options.annotate)
|
+ elif options.generate_json_file:
|
+ return LOG_PROCESSOR_CLASSES['gtest']
|
+
|
+ return None
|
+
|
+
|
+def _GetCommitPos(build_properties):
|
+ """Extracts the commit position from the build properties, if its there."""
|
+ if 'got_revision_cp' not in build_properties:
|
+ return None
|
+ commit_pos = build_properties['got_revision_cp']
|
+ return int(re.search(r'{#(\d+)}', commit_pos).group(1))
|
+
|
+
|
+def _GetMainRevision(options):
|
+ """Return revision to use as the numerical x-value in the perf dashboard.
|
+
|
+ This will be used as the value of "rev" in the data passed to
|
+ results_dashboard.SendResults.
|
+
|
+ In order or priority, this function could return:
|
+ 1. The value of the --revision flag (IF it can be parsed as an int).
|
+ 2. The value of "got_revision_cp" in build properties.
|
+ 3. An SVN number, git commit position, or git commit hash.
|
+ """
|
+ if options.revision and options.revision.isdigit():
|
+ return options.revision
|
+ commit_pos_num = _GetCommitPos(options.build_properties)
|
+ if commit_pos_num is not None:
|
+ return commit_pos_num
|
+ # TODO(sullivan,qyearsley): Don't fall back to _GetRevision if it returns
|
+ # a git commit, since this should be a numerical revision. Instead, abort
|
+ # and fail.
|
+ return _GetRevision(os.path.dirname(os.path.abspath(options.build_dir)))
|
+
|
+
|
+def _GetBlinkRevision(options):
|
+ if options.webkit_revision:
|
+ webkit_revision = options.webkit_revision
|
+ else:
|
+ try:
|
+ webkit_dir = chromium_utils.FindUpward(
|
+ os.path.abspath(options.build_dir), 'third_party', 'WebKit', 'Source')
|
+ webkit_revision = _GetRevision(webkit_dir)
|
+ except Exception:
|
+ webkit_revision = None
|
+ return webkit_revision
|
+
|
+
|
+def _GetTelemetryRevisions(options):
|
+ """Fills in the same revisions fields that process_log_utils does."""
|
+
|
+ versions = {}
|
+ versions['rev'] = _GetMainRevision(options)
|
+ versions['webkit_rev'] = _GetBlinkRevision(options)
|
+ versions['webrtc_rev'] = options.build_properties.get('got_webrtc_revision')
|
+ versions['v8_rev'] = options.build_properties.get('got_v8_revision')
|
+ versions['ver'] = options.build_properties.get('version')
|
+ versions['git_revision'] = options.build_properties.get('git_revision')
|
+ # There are a lot of "bad" revisions to check for, so clean them all up here.
|
+ for key in versions.keys():
|
+ if not versions[key] or versions[key] == 'undefined':
|
+ del versions[key]
|
+ return versions
|
+
|
+
|
+def _CreateLogProcessor(log_processor_class, options, telemetry_info):
|
+ """Creates a log processor instance.
|
+
|
+ Args:
|
+ log_processor_class: A subclass of PerformanceLogProcessor or similar class.
|
+ options: Command-line options (from OptionParser).
|
+ telemetry_info: dict of info for run_benchmark runs.
|
+
|
+ Returns:
|
+ An instance of a log processor class, or None.
|
+ """
|
+ if not log_processor_class:
|
+ return None
|
+
|
+ if log_processor_class.__name__ == 'TelemetryResultsProcessor':
|
+ tracker_obj = log_processor_class(
|
+ telemetry_info['filename'],
|
+ telemetry_info['is_ref'],
|
+ telemetry_info['cleanup_dir'])
|
+ elif log_processor_class.__name__ == 'GTestLogParser':
|
+ tracker_obj = log_processor_class()
|
+ elif log_processor_class.__name__ == 'GTestJSONParser':
|
+ tracker_obj = log_processor_class(
|
+ options.build_properties.get('mastername'))
|
+ else:
|
+ webkit_revision = _GetBlinkRevision(options) or 'undefined'
|
+ revision = _GetMainRevision(options) or 'undefined'
|
+
|
+ tracker_obj = log_processor_class(
|
+ revision=revision,
|
+ build_properties=options.build_properties,
|
+ factory_properties=options.factory_properties,
|
+ webkit_revision=webkit_revision)
|
+
|
+ if options.annotate and options.generate_json_file:
|
+ tracker_obj.ProcessLine(_GetMasterString(_GetMaster()))
|
+
|
+ return tracker_obj
|
+
|
+
|
+def _GetSupplementalColumns(build_dir, supplemental_colummns_file_name):
|
+ """Reads supplemental columns data from a file.
|
+
|
+ Args:
|
+ build_dir: Build dir name.
|
+ supplemental_columns_file_name: Name of a file which contains the
|
+ supplemental columns data (in JSON format).
|
+
|
+ Returns:
|
+ A dict of supplemental data to send to the dashboard.
|
+ """
|
+ supplemental_columns = {}
|
+ supplemental_columns_file = os.path.join(build_dir,
|
+ results_dashboard.CACHE_DIR,
|
+ supplemental_colummns_file_name)
|
+ if os.path.exists(supplemental_columns_file):
|
+ with file(supplemental_columns_file, 'r') as f:
|
+ supplemental_columns = json.loads(f.read())
|
+ return supplemental_columns
|
+
|
+
|
+def _ResultsDashboardDict(options):
|
+ """Generates a dict of info needed by the results dashboard.
|
+
|
+ Args:
|
+ options: Program arguments.
|
+
|
+ Returns:
|
+ dict containing data the dashboard needs.
|
+ """
|
+ build_dir = os.path.abspath(options.build_dir)
|
+ supplemental_columns = _GetSupplementalColumns(
|
+ build_dir, options.supplemental_columns_file)
|
+ extra_columns = options.perf_config
|
+ if extra_columns:
|
+ supplemental_columns.update(extra_columns)
|
+ fields = {
|
+ 'system': _GetPerfID(options),
|
+ 'test': options.test_type,
|
+ 'url': options.results_url,
|
+ 'mastername': options.build_properties.get('mastername'),
|
+ 'buildername': options.build_properties.get('buildername'),
|
+ 'buildnumber': options.build_properties.get('buildnumber'),
|
+ 'build_dir': build_dir,
|
+ 'supplemental_columns': supplemental_columns,
|
+ 'revisions': _GetTelemetryRevisions(options),
|
+ }
|
+ return fields
|
+
|
+
|
+def _GenerateDashboardJson(log_processor, args):
|
+ """Generates chartjson to send to the dashboard.
|
+
|
+ Args:
|
+ log_processor: An instance of a log processor class, which has been used to
|
+ process the test output, so it contains the test results.
|
+ args: Dict of additional args to send to results_dashboard.
|
+ """
|
+ assert log_processor.IsChartJson()
|
+
|
+ chart_json = log_processor.ChartJson()
|
+ if chart_json:
|
+ return results_dashboard.MakeDashboardJsonV1(
|
+ chart_json,
|
+ args['revisions'], args['system'], args['mastername'],
|
+ args['buildername'], args['buildnumber'],
|
+ args['supplemental_columns'], log_processor.IsReferenceBuild())
|
+ return None
|
+
|
+
|
+def _WriteLogProcessorResultsToOutput(log_processor, log_output_file):
|
+ """Writes the log processor's results to a file.
|
+
|
+ Args:
|
+ chartjson_file: Path to the file to write the results.
|
+ log_processor: An instance of a log processor class, which has been used to
|
+ process the test output, so it contains the test results.
|
+ """
|
+ with open(log_output_file, 'w') as f:
|
+ results = {
|
+ 'passed': log_processor.PassedTests(),
|
+ 'failed': log_processor.FailedTests(),
|
+ 'flakes': log_processor.FlakyTests(),
|
+ }
|
+ json.dump(results, f)
|
+
|
+
|
+def _WriteChartJsonToOutput(chartjson_file, log_processor, args):
|
+ """Writes the dashboard chartjson to a file for display in the waterfall.
|
+
|
+ Args:
|
+ chartjson_file: Path to the file to write the chartjson.
|
+ log_processor: An instance of a log processor class, which has been used to
|
+ process the test output, so it contains the test results.
|
+ args: Dict of additional args to send to results_dashboard.
|
+ """
|
+ assert log_processor.IsChartJson()
|
+
|
+ chartjson_data = _GenerateDashboardJson(log_processor, args)
|
+
|
+ with open(chartjson_file, 'w') as f:
|
+ json.dump(chartjson_data, f)
|
+
|
+
|
+def _SendResultsToDashboard(log_processor, args):
|
+ """Sends results from a log processor instance to the dashboard.
|
+
|
+ Args:
|
+ log_processor: An instance of a log processor class, which has been used to
|
+ process the test output, so it contains the test results.
|
+ args: Dict of additional args to send to results_dashboard.
|
+
|
+ Returns:
|
+ True if no errors occurred.
|
+ """
|
+ if args['system'] is None:
|
+ # perf_id not specified in factory properties.
|
+ print 'Error: No system name (perf_id) specified when sending to dashboard.'
|
+ return True
|
+
|
+ results = None
|
+ if log_processor.IsChartJson():
|
+ results = _GenerateDashboardJson(log_processor, args)
|
+ if not results:
|
+ print 'Error: No json output from telemetry.'
|
+ print '@@@STEP_FAILURE@@@'
|
+ log_processor.Cleanup()
|
+ else:
|
+ charts = _GetDataFromLogProcessor(log_processor)
|
+ results = results_dashboard.MakeListOfPoints(
|
+ charts, args['system'], args['test'], args['mastername'],
|
+ args['buildername'], args['buildnumber'], args['supplemental_columns'])
|
+
|
+ if not results:
|
+ return False
|
+
|
+ logging.debug(json.dumps(results, indent=2))
|
+ return results_dashboard.SendResults(results, args['url'], args['build_dir'])
|
+
|
+
|
+def _GetDataFromLogProcessor(log_processor):
|
+ """Returns a mapping of chart names to chart data.
|
+
|
+ Args:
|
+ log_processor: A log processor (aka results tracker) object.
|
+
|
+ Returns:
|
+ A dictionary mapping chart name to lists of chart data.
|
+ put together in log_processor. Each chart data dictionary contains:
|
+ "traces": A dictionary mapping trace names to value, stddev pairs.
|
+ "units": Units for the chart.
|
+ "rev": A revision number or git hash.
|
+ Plus other revision keys, e.g. webkit_rev, ver, v8_rev.
|
+ """
|
+ charts = {}
|
+ for log_file_name, line_list in log_processor.PerformanceLogs().iteritems():
|
+ if not log_file_name.endswith('-summary.dat'):
|
+ # The log processor data also contains "graphs list" file contents,
|
+ # which we can ignore.
|
+ continue
|
+ chart_name = log_file_name.replace('-summary.dat', '')
|
+
|
+ # It's assumed that the log lines list has length one, because for each
|
+ # graph name only one line is added in log_processor in the method
|
+ # GraphingLogProcessor._CreateSummaryOutput.
|
+ if len(line_list) != 1:
|
+ print 'Error: Unexpected log processor line list: %s' % str(line_list)
|
+ continue
|
+ line = line_list[0].rstrip()
|
+ try:
|
+ charts[chart_name] = json.loads(line)
|
+ except ValueError:
|
+ print 'Error: Could not parse JSON: %s' % line
|
+ return charts
|
+
|
+
|
+def _BuildCoverageGtestExclusions(options, args):
|
+ """Appends a list of GTest exclusion filters to the args list."""
|
+ gtest_exclusions = {
|
+ 'win32': {
|
+ 'browser_tests': (
|
+ 'ChromeNotifierDelegateBrowserTest.ClickTest',
|
+ 'ChromeNotifierDelegateBrowserTest.ButtonClickTest',
|
+ 'SyncFileSystemApiTest.GetFileStatuses',
|
+ 'SyncFileSystemApiTest.WriteFileThenGetUsage',
|
+ 'NaClExtensionTest.HostedApp',
|
+ 'MediaGalleriesPlatformAppBrowserTest.MediaGalleriesCopyToNoAccess',
|
+ 'PlatformAppBrowserTest.ComponentAppBackgroundPage',
|
+ 'BookmarksTest.CommandAgainGoesBackToBookmarksTab',
|
+ 'NotificationBitmapFetcherBrowserTest.OnURLFetchFailureTest',
|
+ 'PreservedWindowPlacementIsMigrated.Test',
|
+ 'ShowAppListBrowserTest.ShowAppListFlag',
|
+ '*AvatarMenuButtonTest.*',
|
+ 'NotificationBitmapFetcherBrowserTest.HandleImageFailedTest',
|
+ 'NotificationBitmapFetcherBrowserTest.OnImageDecodedTest',
|
+ 'NotificationBitmapFetcherBrowserTest.StartTest',
|
+ )
|
+ },
|
+ 'darwin2': {},
|
+ 'linux2': {},
|
+ }
|
+ gtest_exclusion_filters = []
|
+ if sys.platform in gtest_exclusions:
|
+ excldict = gtest_exclusions.get(sys.platform)
|
+ if options.test_type in excldict:
|
+ gtest_exclusion_filters = excldict[options.test_type]
|
+ args.append('--gtest_filter=-' + ':'.join(gtest_exclusion_filters))
|
+
|
+
|
+def _UploadProfilingData(options, args):
|
+ """Archives profiling data to Google Storage."""
|
+ # args[1] has --gtest-filter argument.
|
+ if len(args) < 2:
|
+ return 0
|
+
|
+ builder_name = options.build_properties.get('buildername')
|
+ if ((builder_name != 'XP Perf (dbg) (2)' and
|
+ builder_name != 'Linux Perf (lowmem)') or
|
+ options.build_properties.get('mastername') != 'chromium.perf' or
|
+ not options.build_properties.get('got_revision')):
|
+ return 0
|
+
|
+ gtest_filter = args[1]
|
+ if gtest_filter is None:
|
+ return 0
|
+ gtest_name = ''
|
+ if gtest_filter.find('StartupTest.*') > -1:
|
+ gtest_name = 'StartupTest'
|
+ else:
|
+ return 0
|
+
|
+ build_dir = os.path.normpath(os.path.abspath(options.build_dir))
|
+
|
+ # archive_profiling_data.py is in /b/build/scripts/slave and
|
+ # build_dir is /b/build/slave/SLAVE_NAME/build/src/build.
|
+ profiling_archive_tool = os.path.join(build_dir, '..', '..', '..', '..', '..',
|
+ 'scripts', 'slave',
|
+ 'archive_profiling_data.py')
|
+
|
+ if sys.platform == 'win32':
|
+ python = 'python_slave'
|
+ else:
|
+ python = 'python'
|
+
|
+ revision = options.build_properties.get('got_revision')
|
+ cmd = [python, profiling_archive_tool, '--revision', revision,
|
+ '--builder-name', builder_name, '--test-name', gtest_name]
|
+
|
+ return chromium_utils.RunCommand(cmd)
|
+
|
+
|
+def _UploadGtestJsonSummary(json_path, build_properties, test_exe, step_name):
|
+ """Archives GTest results to Google Storage.
|
+
|
+ Args:
|
+ json_path: path to the json-format output of the gtest.
|
+ build_properties: the build properties of a build in buildbot.
|
+ test_exe: the name of the gtest executable.
|
+ step_name: the name of the buildbot step running the gtest.
|
+ """
|
+ if not os.path.exists(json_path):
|
+ return
|
+
|
+ orig_json_data = 'invalid'
|
+ try:
|
+ with open(json_path) as orig_json:
|
+ orig_json_data = json.load(orig_json)
|
+ except ValueError:
|
+ pass
|
+
|
+ target_json = {
|
+ # Increment the version number when making incompatible changes
|
+ # to the layout of this dict. This way clients can recognize different
|
+ # formats instead of guessing.
|
+ 'version': 1,
|
+ 'timestamp': str(datetime.datetime.now()),
|
+ 'test_exe': test_exe,
|
+ 'build_properties': build_properties,
|
+ 'gtest_results': orig_json_data,
|
+ }
|
+ target_json_serialized = json.dumps(target_json, indent=2)
|
+
|
+ now = datetime.datetime.utcnow()
|
+ today = now.date()
|
+ weekly_timestamp = today - datetime.timedelta(days=today.weekday())
|
+
|
+ # Pick a non-colliding file name by hashing the JSON contents
|
+ # (build metadata should be different from build to build).
|
+ target_name = hashlib.sha1(target_json_serialized).hexdigest()
|
+
|
+ # Use a directory structure that makes it easy to filter by year,
|
+ # month, week and day based just on the file path.
|
+ date_json_gs_path = 'gs://chrome-gtest-results/raw/%d/%d/%d/%d/%s.json.gz' % (
|
+ weekly_timestamp.year,
|
+ weekly_timestamp.month,
|
+ weekly_timestamp.day,
|
+ today.day,
|
+ target_name)
|
+
|
+ # Use a directory structure so that the json results could be indexed by
|
+ # master_name/builder_name/build_number/step_name.
|
+ master_name = build_properties.get('mastername')
|
+ builder_name = build_properties.get('buildername')
|
+ build_number = build_properties.get('buildnumber')
|
+ buildbot_json_gs_path = ''
|
+ if (master_name and builder_name and
|
+ (build_number is not None and build_number != '') and step_name):
|
+ # build_number could be zero.
|
+ buildbot_json_gs_path = (
|
+ 'gs://chrome-gtest-results/buildbot/%s/%s/%d/%s.json.gz' % (
|
+ master_name,
|
+ builder_name,
|
+ build_number,
|
+ step_name))
|
+
|
+ fd, target_json_path = tempfile.mkstemp()
|
+ try:
|
+ with os.fdopen(fd, 'w') as f:
|
+ with gzip.GzipFile(fileobj=f, compresslevel=9) as gzipf:
|
+ gzipf.write(target_json_serialized)
|
+
|
+ slave_utils.GSUtilCopy(target_json_path, date_json_gs_path)
|
+ if buildbot_json_gs_path:
|
+ slave_utils.GSUtilCopy(target_json_path, buildbot_json_gs_path)
|
+ finally:
|
+ os.remove(target_json_path)
|
+
|
+ if target_json['gtest_results'] == 'invalid':
|
+ return
|
+
|
+ # Use a directory structure that makes it easy to filter by year,
|
+ # month, week and day based just on the file path.
|
+ bigquery_json_gs_path = (
|
+ 'gs://chrome-gtest-results/bigquery/%d/%d/%d/%d/%s.json.gz' % (
|
+ weekly_timestamp.year,
|
+ weekly_timestamp.month,
|
+ weekly_timestamp.day,
|
+ today.day,
|
+ target_name))
|
+
|
+ fd, bigquery_json_path = tempfile.mkstemp()
|
+ try:
|
+ with os.fdopen(fd, 'w') as f:
|
+ with gzip.GzipFile(fileobj=f, compresslevel=9) as gzipf:
|
+ for iteration_data in (
|
+ target_json['gtest_results']['per_iteration_data']):
|
+ for test_name, test_runs in iteration_data.iteritems():
|
+ # Compute the number of flaky failures. A failure is only considered
|
+ # flaky, when the test succeeds at least once on the same code.
|
+ # However, we do not consider a test flaky if it only changes
|
+ # between various failure states, e.g. FAIL and TIMEOUT.
|
+ num_successes = len([r['status'] for r in test_runs
|
+ if r['status'] == 'SUCCESS'])
|
+ num_failures = len(test_runs) - num_successes
|
+ if num_failures > 0 and num_successes > 0:
|
+ flaky_failures = num_failures
|
+ else:
|
+ flaky_failures = 0
|
+
|
+ for run_index, run_data in enumerate(test_runs):
|
+ row = {
|
+ 'test_name': test_name,
|
+ 'run_index': run_index,
|
+ 'elapsed_time_ms': run_data['elapsed_time_ms'],
|
+ 'status': run_data['status'],
|
+ 'test_exe': target_json['test_exe'],
|
+ 'global_tags': target_json['gtest_results']['global_tags'],
|
+ 'slavename':
|
+ target_json['build_properties'].get('slavename', ''),
|
+ 'buildername':
|
+ target_json['build_properties'].get('buildername', ''),
|
+ 'mastername':
|
+ target_json['build_properties'].get('mastername', ''),
|
+ 'raw_json_gs_path': date_json_gs_path,
|
+ 'timestamp': now.strftime('%Y-%m-%d %H:%M:%S.%f'),
|
+ 'flaky_failures': flaky_failures,
|
+ 'num_successes': num_successes,
|
+ 'num_failures': num_failures
|
+ }
|
+ gzipf.write(json.dumps(row) + '\n')
|
+
|
+ slave_utils.GSUtilCopy(bigquery_json_path, bigquery_json_gs_path)
|
+ finally:
|
+ os.remove(bigquery_json_path)
|
+
|
+
|
+def _GenerateRunIsolatedCommand(build_dir, test_exe_path, options, command):
|
+ """Converts the command to run through the run isolate script.
|
+
|
+ All commands are sent through the run isolated script, in case
|
+ they need to be run in isolate mode.
|
+ """
|
+ run_isolated_test = os.path.join(BASE_DIR, 'runisolatedtest.py')
|
+ isolate_command = [
|
+ sys.executable, run_isolated_test,
|
+ '--test_name', options.test_type,
|
+ '--builder_name', options.build_properties.get('buildername', ''),
|
+ '--checkout_dir', os.path.dirname(os.path.dirname(build_dir)),
|
+ ]
|
+ if options.factory_properties.get('force_isolated'):
|
+ isolate_command += ['--force-isolated']
|
+ isolate_command += [test_exe_path, '--'] + command
|
+
|
+ return isolate_command
|
+
|
+
|
+def _GetPerfID(options):
|
+ if options.perf_id:
|
+ perf_id = options.perf_id
|
+ else:
|
+ perf_id = options.factory_properties.get('perf_id')
|
+ if options.factory_properties.get('add_perf_id_suffix'):
|
+ perf_id += options.build_properties.get('perf_id_suffix')
|
+ return perf_id
|
+
|
+
|
+def _GetSanitizerSymbolizeCommand(strip_path_prefix=None, json_file_name=None):
|
+ script_path = os.path.abspath(os.path.join('src', 'tools', 'valgrind',
|
+ 'asan', 'asan_symbolize.py'))
|
+ command = [sys.executable, script_path]
|
+ if strip_path_prefix:
|
+ command.append(strip_path_prefix)
|
+ if json_file_name:
|
+ command.append('--test-summary-json-file=%s' % json_file_name)
|
+ return command
|
+
|
+
|
+def _SymbolizeSnippetsInJSON(options, json_file_name):
|
+ if not json_file_name:
|
+ return
|
+ symbolize_command = _GetSanitizerSymbolizeCommand(
|
+ strip_path_prefix=options.strip_path_prefix,
|
+ json_file_name=json_file_name)
|
+ try:
|
+ p = subprocess.Popen(symbolize_command, stderr=subprocess.PIPE)
|
+ (_, stderr) = p.communicate()
|
+ except OSError as e:
|
+ print 'Exception while symbolizing snippets: %s' % e
|
+
|
+ if p.returncode != 0:
|
+ print "Error: failed to symbolize snippets in JSON:\n"
|
+ print stderr
|
+
|
+
|
+def _MainParse(options, _args):
|
+ """Run input through annotated test parser.
|
+
|
+ This doesn't execute a test, but reads test input from a file and runs it
|
+ through the specified annotation parser (aka log processor).
|
+ """
|
+ if not options.annotate:
|
+ raise chromium_utils.MissingArgument('--parse-input doesn\'t make sense '
|
+ 'without --annotate.')
|
+
|
+ # If --annotate=list was passed, list the log processor classes and exit.
|
+ if _ListLogProcessors(options.annotate):
|
+ return 0
|
+
|
+ log_processor_class = _SelectLogProcessor(options, False)
|
+ log_processor = _CreateLogProcessor(log_processor_class, options, None)
|
+
|
+ if options.generate_json_file:
|
+ if os.path.exists(options.test_output_xml):
|
+ # remove the old XML output file.
|
+ os.remove(options.test_output_xml)
|
+
|
+ if options.parse_input == '-':
|
+ f = sys.stdin
|
+ else:
|
+ try:
|
+ f = open(options.parse_input, 'rb')
|
+ except IOError as e:
|
+ print 'Error %d opening \'%s\': %s' % (e.errno, options.parse_input,
|
+ e.strerror)
|
+ return 1
|
+
|
+ with f:
|
+ for line in f:
|
+ log_processor.ProcessLine(line)
|
+
|
+ if options.generate_json_file:
|
+ if not _GenerateJSONForTestResults(options, log_processor):
|
+ return 1
|
+
|
+ if options.annotate:
|
+ annotation_utils.annotate(
|
+ options.test_type, options.parse_result, log_processor,
|
+ perf_dashboard_id=options.perf_dashboard_id)
|
+
|
+ return options.parse_result
|
+
|
+
|
+def _MainMac(options, args, extra_env):
|
+ """Runs the test on mac."""
|
+ if len(args) < 1:
|
+ raise chromium_utils.MissingArgument('Usage: %s' % USAGE)
|
+
|
+ telemetry_info = _UpdateRunBenchmarkArgs(args, options)
|
+ test_exe = args[0]
|
+ if options.run_python_script:
|
+ build_dir = os.path.normpath(os.path.abspath(options.build_dir))
|
+ test_exe_path = test_exe
|
+ else:
|
+ build_dir = os.path.normpath(os.path.abspath(options.build_dir))
|
+ test_exe_path = os.path.join(build_dir, options.target, test_exe)
|
+
|
+ # Nuke anything that appears to be stale chrome items in the temporary
|
+ # directory from previous test runs (i.e.- from crashes or unittest leaks).
|
+ slave_utils.RemoveChromeTemporaryFiles()
|
+
|
+ if options.run_shell_script:
|
+ command = ['bash', test_exe_path]
|
+ elif options.run_python_script:
|
+ command = [sys.executable, test_exe]
|
+ else:
|
+ command = _BuildTestBinaryCommand(build_dir, test_exe_path, options)
|
+ command.extend(args[1:])
|
+
|
+ # If --annotate=list was passed, list the log processor classes and exit.
|
+ if _ListLogProcessors(options.annotate):
|
+ return 0
|
+ log_processor_class = _SelectLogProcessor(options, bool(telemetry_info))
|
+ log_processor = _CreateLogProcessor(
|
+ log_processor_class, options, telemetry_info)
|
+
|
+ if options.generate_json_file:
|
+ if os.path.exists(options.test_output_xml):
|
+ # remove the old XML output file.
|
+ os.remove(options.test_output_xml)
|
+
|
+ try:
|
+ if _UsingGtestJson(options):
|
+ json_file_name = log_processor.PrepareJSONFile(
|
+ options.test_launcher_summary_output)
|
+ command.append('--test-launcher-summary-output=%s' % json_file_name)
|
+
|
+ pipes = []
|
+ if options.use_symbolization_script:
|
+ pipes = [_GetSanitizerSymbolizeCommand()]
|
+
|
+ command = _GenerateRunIsolatedCommand(build_dir, test_exe_path, options,
|
+ command)
|
+ result = _RunGTestCommand(options, command, extra_env, pipes=pipes,
|
+ log_processor=log_processor)
|
+ finally:
|
+ if _UsingGtestJson(options):
|
+ _UploadGtestJsonSummary(json_file_name,
|
+ options.build_properties,
|
+ test_exe,
|
+ options.step_name)
|
+ log_processor.ProcessJSONFile(options.build_dir)
|
+
|
+ if options.generate_json_file:
|
+ if not _GenerateJSONForTestResults(options, log_processor):
|
+ return 1
|
+
|
+ if options.annotate:
|
+ annotation_utils.annotate(
|
+ options.test_type, result, log_processor,
|
+ perf_dashboard_id=options.perf_dashboard_id)
|
+
|
+ if options.chartjson_file and telemetry_info:
|
+ _WriteChartJsonToOutput(options.chartjson_file,
|
+ log_processor,
|
+ _ResultsDashboardDict(options))
|
+
|
+ if options.results_url:
|
+ if not _SendResultsToDashboard(
|
+ log_processor, _ResultsDashboardDict(options)):
|
+ return 1
|
+
|
+ return result
|
+
|
+
|
+def _MainIOS(options, args, extra_env):
|
+ """Runs the test on iOS."""
|
+ if len(args) < 1:
|
+ raise chromium_utils.MissingArgument('Usage: %s' % USAGE)
|
+
|
+ def kill_simulator():
|
+ chromium_utils.RunCommand(['/usr/bin/killall', 'iPhone Simulator'])
|
+
|
+ # For iOS tests, the args come in in the following order:
|
+ # [0] test display name formatted as 'test_name (device[ ios_version])'
|
+ # [1:] gtest args (e.g. --gtest_print_time)
|
+
|
+ # Set defaults in case the device family and iOS version can't be parsed out
|
+ # of |args|
|
+ device = 'iPhone Retina (4-inch)'
|
+ ios_version = '7.1'
|
+
|
+ # Parse the test_name and device from the test display name.
|
+ # The expected format is: <test_name> (<device>)
|
+ result = re.match(r'(.*) \((.*)\)$', args[0])
|
+ if result is not None:
|
+ test_name, device = result.groups()
|
+ # Check if the device has an iOS version. The expected format is:
|
+ # <device_name><space><ios_version>, where ios_version may have 2 or 3
|
+ # numerals (e.g. '4.3.11' or '5.0').
|
+ result = re.match(r'(.*) (\d+\.\d+(\.\d+)?)$', device)
|
+ if result is not None:
|
+ device = result.groups()[0]
|
+ ios_version = result.groups()[1]
|
+ else:
|
+ # If first argument is not in the correct format, log a warning but
|
+ # fall back to assuming the first arg is the test_name and just run
|
+ # on the iphone simulator.
|
+ test_name = args[0]
|
+ print ('Can\'t parse test name, device, and iOS version. '
|
+ 'Running %s on %s %s' % (test_name, device, ios_version))
|
+
|
+ # Build the args for invoking iossim, which will install the app on the
|
+ # simulator and launch it, then dump the test results to stdout.
|
+
|
+ build_dir = os.path.normpath(os.path.abspath(options.build_dir))
|
+ app_exe_path = os.path.join(
|
+ build_dir, options.target + '-iphonesimulator', test_name + '.app')
|
+ test_exe_path = os.path.join(
|
+ build_dir, 'ninja-iossim', options.target, 'iossim')
|
+ tmpdir = tempfile.mkdtemp()
|
+ command = [test_exe_path,
|
+ '-d', device,
|
+ '-s', ios_version,
|
+ '-t', '120',
|
+ '-u', tmpdir,
|
+ app_exe_path, '--'
|
+ ]
|
+ command.extend(args[1:])
|
+
|
+ # If --annotate=list was passed, list the log processor classes and exit.
|
+ if _ListLogProcessors(options.annotate):
|
+ return 0
|
+ log_processor = _CreateLogProcessor(
|
+ LOG_PROCESSOR_CLASSES['gtest'], options, None)
|
+
|
+ # Make sure the simulator isn't running.
|
+ kill_simulator()
|
+
|
+ # Nuke anything that appears to be stale chrome items in the temporary
|
+ # directory from previous test runs (i.e.- from crashes or unittest leaks).
|
+ slave_utils.RemoveChromeTemporaryFiles()
|
+
|
+ dirs_to_cleanup = [tmpdir]
|
+ crash_files_before = set([])
|
+ crash_files_after = set([])
|
+ crash_files_before = set(crash_utils.list_crash_logs())
|
+
|
+ result = _RunGTestCommand(options, command, extra_env, log_processor)
|
+
|
+ # Because test apps kill themselves, iossim sometimes returns non-zero
|
+ # status even though all tests have passed. Check the log_processor to
|
+ # see if the test run was successful.
|
+ if log_processor.CompletedWithoutFailure():
|
+ result = 0
|
+ else:
|
+ result = 1
|
+
|
+ if result != 0:
|
+ crash_utils.wait_for_crash_logs()
|
+ crash_files_after = set(crash_utils.list_crash_logs())
|
+
|
+ kill_simulator()
|
+
|
+ new_crash_files = crash_files_after.difference(crash_files_before)
|
+ crash_utils.print_new_crash_files(new_crash_files)
|
+
|
+ for a_dir in dirs_to_cleanup:
|
+ try:
|
+ chromium_utils.RemoveDirectory(a_dir)
|
+ except OSError as e:
|
+ print >> sys.stderr, e
|
+ # Don't fail.
|
+
|
+ return result
|
+
|
+
|
+def _MainLinux(options, args, extra_env):
|
+ """Runs the test on Linux."""
|
+ import platform
|
+ xvfb_path = os.path.join(os.path.dirname(sys.argv[0]), '..', '..',
|
+ 'third_party', 'xvfb', platform.architecture()[0])
|
+
|
+ if len(args) < 1:
|
+ raise chromium_utils.MissingArgument('Usage: %s' % USAGE)
|
+
|
+ build_dir = os.path.normpath(os.path.abspath(options.build_dir))
|
+ if options.slave_name:
|
+ slave_name = options.slave_name
|
+ else:
|
+ slave_name = slave_utils.SlaveBuildName(build_dir)
|
+ bin_dir = os.path.join(build_dir, options.target)
|
+
|
+ # Figure out what we want for a special frame buffer directory.
|
+ special_xvfb_dir = None
|
+ fp_chromeos = options.factory_properties.get('chromeos', None)
|
+ if (fp_chromeos or
|
+ slave_utils.GypFlagIsOn(options, 'use_aura') or
|
+ slave_utils.GypFlagIsOn(options, 'chromeos')):
|
+ special_xvfb_dir = xvfb_path
|
+
|
+ telemetry_info = _UpdateRunBenchmarkArgs(args, options)
|
+ test_exe = args[0]
|
+ if options.run_python_script:
|
+ test_exe_path = test_exe
|
+ else:
|
+ test_exe_path = os.path.join(bin_dir, test_exe)
|
+ if not os.path.exists(test_exe_path):
|
+ if options.factory_properties.get('succeed_on_missing_exe', False):
|
+ print '%s missing but succeed_on_missing_exe used, exiting' % (
|
+ test_exe_path)
|
+ return 0
|
+ msg = 'Unable to find %s' % test_exe_path
|
+ raise chromium_utils.PathNotFound(msg)
|
+
|
+ # Unset http_proxy and HTTPS_PROXY environment variables. When set, this
|
+ # causes some tests to hang. See http://crbug.com/139638 for more info.
|
+ if 'http_proxy' in os.environ:
|
+ del os.environ['http_proxy']
|
+ print 'Deleted http_proxy environment variable.'
|
+ if 'HTTPS_PROXY' in os.environ:
|
+ del os.environ['HTTPS_PROXY']
|
+ print 'Deleted HTTPS_PROXY environment variable.'
|
+
|
+ # Path to SUID sandbox binary. This must be installed on all bots.
|
+ extra_env['CHROME_DEVEL_SANDBOX'] = CHROME_SANDBOX_PATH
|
+
|
+ # Nuke anything that appears to be stale chrome items in the temporary
|
+ # directory from previous test runs (i.e.- from crashes or unittest leaks).
|
+ slave_utils.RemoveChromeTemporaryFiles()
|
+
|
+ extra_env['LD_LIBRARY_PATH'] = ''
|
+
|
+ if options.enable_lsan:
|
+ # Use the debug version of libstdc++ under LSan. If we don't, there will be
|
+ # a lot of incomplete stack traces in the reports.
|
+ extra_env['LD_LIBRARY_PATH'] += '/usr/lib/x86_64-linux-gnu/debug:'
|
+
|
+ extra_env['LD_LIBRARY_PATH'] += '%s:%s/lib:%s/lib.target' % (bin_dir, bin_dir,
|
+ bin_dir)
|
+
|
+ if options.run_shell_script:
|
+ command = ['bash', test_exe_path]
|
+ elif options.run_python_script:
|
+ command = [sys.executable, test_exe]
|
+ else:
|
+ command = _BuildTestBinaryCommand(build_dir, test_exe_path, options)
|
+ command.extend(args[1:])
|
+
|
+ # If --annotate=list was passed, list the log processor classes and exit.
|
+ if _ListLogProcessors(options.annotate):
|
+ return 0
|
+ log_processor_class = _SelectLogProcessor(options, bool(telemetry_info))
|
+ log_processor = _CreateLogProcessor(
|
+ log_processor_class, options, telemetry_info)
|
+
|
+ if options.generate_json_file:
|
+ if os.path.exists(options.test_output_xml):
|
+ # remove the old XML output file.
|
+ os.remove(options.test_output_xml)
|
+
|
+ try:
|
+ start_xvfb = False
|
+ json_file_name = None
|
+
|
+ # TODO(dpranke): checking on test_exe is a temporary hack until we
|
+ # can change the buildbot master to pass --xvfb instead of --no-xvfb
|
+ # for these two steps. See
|
+ # https://code.google.com/p/chromium/issues/detail?id=179814
|
+ start_xvfb = (options.xvfb or
|
+ 'layout_test_wrapper' in test_exe or
|
+ 'devtools_perf_test_wrapper' in test_exe)
|
+ if start_xvfb:
|
+ xvfb.StartVirtualX(
|
+ slave_name, bin_dir,
|
+ with_wm=(options.factory_properties.get('window_manager', 'True') ==
|
+ 'True'),
|
+ server_dir=special_xvfb_dir)
|
+
|
+ if _UsingGtestJson(options):
|
+ json_file_name = log_processor.PrepareJSONFile(
|
+ options.test_launcher_summary_output)
|
+ command.append('--test-launcher-summary-output=%s' % json_file_name)
|
+
|
+ pipes = []
|
+ # See the comment in main() regarding offline symbolization.
|
+ if options.use_symbolization_script:
|
+ symbolize_command = _GetSanitizerSymbolizeCommand(
|
+ strip_path_prefix=options.strip_path_prefix)
|
+ pipes = [symbolize_command]
|
+
|
+ command = _GenerateRunIsolatedCommand(build_dir, test_exe_path, options,
|
+ command)
|
+ result = _RunGTestCommand(options, command, extra_env, pipes=pipes,
|
+ log_processor=log_processor)
|
+ finally:
|
+ if start_xvfb:
|
+ xvfb.StopVirtualX(slave_name)
|
+ if _UsingGtestJson(options):
|
+ if options.use_symbolization_script:
|
+ _SymbolizeSnippetsInJSON(options, json_file_name)
|
+ if json_file_name:
|
+ _UploadGtestJsonSummary(json_file_name,
|
+ options.build_properties,
|
+ test_exe,
|
+ options.step_name)
|
+ log_processor.ProcessJSONFile(options.build_dir)
|
+
|
+ if options.generate_json_file:
|
+ if not _GenerateJSONForTestResults(options, log_processor):
|
+ return 1
|
+
|
+ if options.annotate:
|
+ annotation_utils.annotate(
|
+ options.test_type, result, log_processor,
|
+ perf_dashboard_id=options.perf_dashboard_id)
|
+
|
+ if options.chartjson_file and telemetry_info:
|
+ _WriteChartJsonToOutput(options.chartjson_file,
|
+ log_processor,
|
+ _ResultsDashboardDict(options))
|
+
|
+ if options.results_url:
|
+ if not _SendResultsToDashboard(
|
+ log_processor, _ResultsDashboardDict(options)):
|
+ return 1
|
+
|
+ return result
|
+
|
+
|
+def _MainWin(options, args, extra_env):
|
+ """Runs tests on windows.
|
+
|
+ Using the target build configuration, run the executable given in the
|
+ first non-option argument, passing any following arguments to that
|
+ executable.
|
+
|
+ Args:
|
+ options: Command-line options for this invocation of runtest.py.
|
+ args: Command and arguments for the test.
|
+ extra_env: A dictionary of extra environment variables to set.
|
+
|
+ Returns:
|
+ Exit status code.
|
+ """
|
+ if len(args) < 1:
|
+ raise chromium_utils.MissingArgument('Usage: %s' % USAGE)
|
+
|
+ telemetry_info = _UpdateRunBenchmarkArgs(args, options)
|
+ test_exe = args[0]
|
+ build_dir = os.path.abspath(options.build_dir)
|
+ if options.run_python_script:
|
+ test_exe_path = test_exe
|
+ else:
|
+ test_exe_path = os.path.join(build_dir, options.target, test_exe)
|
+
|
+ if not os.path.exists(test_exe_path):
|
+ if options.factory_properties.get('succeed_on_missing_exe', False):
|
+ print '%s missing but succeed_on_missing_exe used, exiting' % (
|
+ test_exe_path)
|
+ return 0
|
+ raise chromium_utils.PathNotFound('Unable to find %s' % test_exe_path)
|
+
|
+ if options.run_python_script:
|
+ command = [sys.executable, test_exe]
|
+ else:
|
+ command = _BuildTestBinaryCommand(build_dir, test_exe_path, options)
|
+
|
+ command.extend(args[1:])
|
+
|
+ # Nuke anything that appears to be stale chrome items in the temporary
|
+ # directory from previous test runs (i.e.- from crashes or unittest leaks).
|
+ slave_utils.RemoveChromeTemporaryFiles()
|
+
|
+ # If --annotate=list was passed, list the log processor classes and exit.
|
+ if _ListLogProcessors(options.annotate):
|
+ return 0
|
+ log_processor_class = _SelectLogProcessor(options, bool(telemetry_info))
|
+ log_processor = _CreateLogProcessor(
|
+ log_processor_class, options, telemetry_info)
|
+
|
+ if options.generate_json_file:
|
+ if os.path.exists(options.test_output_xml):
|
+ # remove the old XML output file.
|
+ os.remove(options.test_output_xml)
|
+
|
+ try:
|
+ if _UsingGtestJson(options):
|
+ json_file_name = log_processor.PrepareJSONFile(
|
+ options.test_launcher_summary_output)
|
+ command.append('--test-launcher-summary-output=%s' % json_file_name)
|
+
|
+ command = _GenerateRunIsolatedCommand(build_dir, test_exe_path, options,
|
+ command)
|
+ result = _RunGTestCommand(options, command, extra_env, log_processor)
|
+ finally:
|
+ if _UsingGtestJson(options):
|
+ _UploadGtestJsonSummary(json_file_name,
|
+ options.build_properties,
|
+ test_exe,
|
+ options.step_name)
|
+ log_processor.ProcessJSONFile(options.build_dir)
|
+
|
+ if options.generate_json_file:
|
+ if not _GenerateJSONForTestResults(options, log_processor):
|
+ return 1
|
+
|
+ if options.annotate:
|
+ annotation_utils.annotate(
|
+ options.test_type, result, log_processor,
|
+ perf_dashboard_id=options.perf_dashboard_id)
|
+
|
+ if options.chartjson_file and telemetry_info:
|
+ _WriteChartJsonToOutput(options.chartjson_file,
|
+ log_processor,
|
+ _ResultsDashboardDict(options))
|
+
|
+ if options.results_url:
|
+ if not _SendResultsToDashboard(
|
+ log_processor, _ResultsDashboardDict(options)):
|
+ return 1
|
+
|
+ return result
|
+
|
+
|
+def _MainAndroid(options, args, extra_env):
|
+ """Runs tests on android.
|
+
|
+ Running GTest-based tests on android is different than on Linux as it requires
|
+ src/build/android/test_runner.py to deploy and communicate with the device.
|
+ Python scripts are the same as with Linux.
|
+
|
+ Args:
|
+ options: Command-line options for this invocation of runtest.py.
|
+ args: Command and arguments for the test.
|
+ extra_env: A dictionary of extra environment variables to set.
|
+
|
+ Returns:
|
+ Exit status code.
|
+ """
|
+ if options.run_python_script:
|
+ return _MainLinux(options, args, extra_env)
|
+
|
+ if len(args) < 1:
|
+ raise chromium_utils.MissingArgument('Usage: %s' % USAGE)
|
+
|
+ if _ListLogProcessors(options.annotate):
|
+ return 0
|
+ log_processor_class = _SelectLogProcessor(options, False)
|
+ log_processor = _CreateLogProcessor(log_processor_class, options, None)
|
+
|
+ if options.generate_json_file:
|
+ if os.path.exists(options.test_output_xml):
|
+ # remove the old XML output file.
|
+ os.remove(options.test_output_xml)
|
+
|
+ # Assume it's a gtest apk, so use the android harness.
|
+ test_suite = args[0]
|
+ run_test_target_option = '--release'
|
+ if options.target == 'Debug':
|
+ run_test_target_option = '--debug'
|
+ command = ['src/build/android/test_runner.py', 'gtest',
|
+ run_test_target_option, '-s', test_suite]
|
+
|
+ if options.flakiness_dashboard_server:
|
+ command += ['--flakiness-dashboard-server=%s' %
|
+ options.flakiness_dashboard_server]
|
+
|
+ result = _RunGTestCommand(
|
+ options, command, extra_env, log_processor=log_processor)
|
+
|
+ if options.generate_json_file:
|
+ if not _GenerateJSONForTestResults(options, log_processor):
|
+ return 1
|
+
|
+ if options.annotate:
|
+ annotation_utils.annotate(
|
+ options.test_type, result, log_processor,
|
+ perf_dashboard_id=options.perf_dashboard_id)
|
+
|
+ if options.results_url:
|
+ if not _SendResultsToDashboard(
|
+ log_processor, _ResultsDashboardDict(options)):
|
+ return 1
|
+
|
+ return result
|
+
|
+
|
+def _UpdateRunBenchmarkArgs(args, options):
|
+ """Updates the arguments for telemetry run_benchmark commands.
|
+
|
+ Ensures that --output=chartjson is set and adds a --output argument.
|
+
|
+ Arguments:
|
+ args: list of command line arguments, starts with 'run_benchmark' for
|
+ telemetry tests.
|
+
|
+ Returns:
|
+ None if not a telemetry test, otherwise a
|
+ dict containing the output filename and whether it is a reference build.
|
+ """
|
+ if not options.chartjson_file:
|
+ return {}
|
+
|
+ if args[0].endswith('run_benchmark'):
|
+ is_ref = '--browser=reference' in args
|
+ output_dir = tempfile.mkdtemp()
|
+ args.extend(['--output-dir=%s' % output_dir])
|
+ temp_filename = os.path.join(output_dir, 'results-chart.json')
|
+ return {'filename': temp_filename, 'is_ref': is_ref, 'cleanup_dir': True}
|
+ elif args[0].endswith('test_runner.py'):
|
+ (_, temp_json_filename) = tempfile.mkstemp()
|
+ args.extend(['--output-chartjson-data=%s' % temp_json_filename])
|
+ return {'filename': temp_json_filename,
|
+ 'is_ref': False,
|
+ 'cleanup_dir': False}
|
+
|
+ return None
|
+
|
+
|
+def _ConfigureSanitizerTools(options, args, extra_env):
|
+ if (options.enable_asan or options.enable_tsan or
|
+ options.enable_msan or options.enable_lsan):
|
+ # Instruct GTK to use malloc while running ASan, TSan, MSan or LSan tests.
|
+ extra_env['G_SLICE'] = 'always-malloc'
|
+ extra_env['NSS_DISABLE_ARENA_FREE_LIST'] = '1'
|
+ extra_env['NSS_DISABLE_UNLOAD'] = '1'
|
+
|
+ symbolizer_path = os.path.abspath(os.path.join('src', 'third_party',
|
+ 'llvm-build', 'Release+Asserts', 'bin', 'llvm-symbolizer'))
|
+ disable_sandbox_flag = '--no-sandbox'
|
+ if args and 'layout_test_wrapper' in args[0]:
|
+ disable_sandbox_flag = '--additional-drt-flag=%s' % disable_sandbox_flag
|
+
|
+ # Symbolization of sanitizer reports.
|
+ if sys.platform in ['win32', 'cygwin']:
|
+ # On Windows, the in-process symbolizer works even when sandboxed.
|
+ symbolization_options = []
|
+ elif options.enable_tsan or options.enable_lsan:
|
+ # TSan and LSan are not sandbox-compatible, so we can use online
|
+ # symbolization. In fact, they need symbolization to be able to apply
|
+ # suppressions.
|
+ symbolization_options = ['symbolize=1',
|
+ 'external_symbolizer_path=%s' % symbolizer_path,
|
+ 'strip_path_prefix=%s' % options.strip_path_prefix]
|
+ elif options.enable_asan or options.enable_msan:
|
+ # ASan and MSan use a script for offline symbolization.
|
+ # Important note: when running ASan or MSan with leak detection enabled,
|
+ # we must use the LSan symbolization options above.
|
+ symbolization_options = ['symbolize=0']
|
+ # Set the path to llvm-symbolizer to be used by asan_symbolize.py
|
+ extra_env['LLVM_SYMBOLIZER_PATH'] = symbolizer_path
|
+ options.use_symbolization_script = True
|
+
|
+ def AddToExistingEnv(env_dict, key, options_list):
|
+ # Adds a key to the supplied environment dictionary but appends it to
|
+ # existing environment variables if it already contains values.
|
+ assert type(env_dict) is dict
|
+ assert type(options_list) is list
|
+ env_dict[key] = ' '.join(filter(bool, [os.environ.get(key)]+options_list))
|
+
|
+ # ThreadSanitizer
|
+ if options.enable_tsan:
|
+ tsan_options = symbolization_options
|
+ AddToExistingEnv(extra_env, 'TSAN_OPTIONS', tsan_options)
|
+ # Disable sandboxing under TSan for now. http://crbug.com/223602.
|
+ args.append(disable_sandbox_flag)
|
+
|
+ # LeakSanitizer
|
+ if options.enable_lsan:
|
+ # Symbolization options set here take effect only for standalone LSan.
|
+ lsan_options = symbolization_options
|
+ AddToExistingEnv(extra_env, 'LSAN_OPTIONS', lsan_options)
|
+
|
+ # Disable sandboxing under LSan.
|
+ args.append(disable_sandbox_flag)
|
+
|
+ # AddressSanitizer
|
+ if options.enable_asan:
|
+ asan_options = symbolization_options
|
+ if options.enable_lsan:
|
+ asan_options += ['detect_leaks=1']
|
+ AddToExistingEnv(extra_env, 'ASAN_OPTIONS', asan_options)
|
+
|
+ # MemorySanitizer
|
+ if options.enable_msan:
|
+ msan_options = symbolization_options
|
+ if options.enable_lsan:
|
+ msan_options += ['detect_leaks=1']
|
+ AddToExistingEnv(extra_env, 'MSAN_OPTIONS', msan_options)
|
+
|
+
|
+def main():
|
+ """Entry point for runtest.py.
|
+
|
+ This function:
|
+ (1) Sets up the command-line options.
|
+ (2) Sets environment variables based on those options.
|
+ (3) Delegates to the platform-specific main functions.
|
+
|
+ Returns:
|
+ Exit code for this script.
|
+ """
|
+ option_parser = optparse.OptionParser(usage=USAGE)
|
+
|
+ # Since the trailing program to run may have has command-line args of its
|
+ # own, we need to stop parsing when we reach the first positional argument.
|
+ option_parser.disable_interspersed_args()
|
+
|
+ option_parser.add_option('--target', default='Release',
|
+ help='build target (Debug or Release)')
|
+ option_parser.add_option('--pass-target', action='store_true', default=False,
|
+ help='pass --target to the spawned test script')
|
+ option_parser.add_option('--build-dir', help='ignored')
|
+ option_parser.add_option('--pass-build-dir', action='store_true',
|
+ default=False,
|
+ help='pass --build-dir to the spawned test script')
|
+ option_parser.add_option('--test-platform',
|
+ help='Platform to test on, e.g. ios-simulator')
|
+ option_parser.add_option('--total-shards', dest='total_shards',
|
+ default=None, type='int',
|
+ help='Number of shards to split this test into.')
|
+ option_parser.add_option('--shard-index', dest='shard_index',
|
+ default=None, type='int',
|
+ help='Shard to run. Must be between 1 and '
|
+ 'total-shards.')
|
+ option_parser.add_option('--run-shell-script', action='store_true',
|
+ default=False,
|
+ help='treat first argument as the shell script'
|
+ 'to run.')
|
+ option_parser.add_option('--run-python-script', action='store_true',
|
+ default=False,
|
+ help='treat first argument as a python script'
|
+ 'to run.')
|
+ option_parser.add_option('--generate-json-file', action='store_true',
|
+ default=False,
|
+ help='output JSON results file if specified.')
|
+ option_parser.add_option('--xvfb', action='store_true', dest='xvfb',
|
+ default=True,
|
+ help='Start virtual X server on Linux.')
|
+ option_parser.add_option('--no-xvfb', action='store_false', dest='xvfb',
|
+ help='Do not start virtual X server on Linux.')
|
+ option_parser.add_option('-o', '--results-directory', default='',
|
+ help='output results directory for JSON file.')
|
+ option_parser.add_option('--chartjson-file', default='',
|
+ help='File to dump chartjson results.')
|
+ option_parser.add_option('--log-processor-output-file', default='',
|
+ help='File to dump gtest log processor results.')
|
+ option_parser.add_option('--builder-name', default=None,
|
+ help='The name of the builder running this script.')
|
+ option_parser.add_option('--slave-name', default=None,
|
+ help='The name of the slave running this script.')
|
+ option_parser.add_option('--master-class-name', default=None,
|
+ help='The class name of the buildbot master running '
|
+ 'this script: examples include "Chromium", '
|
+ '"ChromiumWebkit", and "ChromiumGPU". The '
|
+ 'flakiness dashboard uses this value to '
|
+ 'categorize results. See buildershandler.py '
|
+ 'in the flakiness dashboard code '
|
+ '(use codesearch) for the known values. '
|
+ 'Defaults to fetching it from '
|
+ 'slaves.cfg/builders.pyl.')
|
+ option_parser.add_option('--build-number', default=None,
|
+ help=('The build number of the builder running'
|
+ 'this script.'))
|
+ option_parser.add_option('--step-name', default=None,
|
+ help=('The name of the step running this script.'))
|
+ option_parser.add_option('--test-type', default='',
|
+ help='The test name that identifies the test, '
|
+ 'e.g. \'unit-tests\'')
|
+ option_parser.add_option('--test-results-server', default='',
|
+ help='The test results server to upload the '
|
+ 'results.')
|
+ option_parser.add_option('--annotate', default='',
|
+ help='Annotate output when run as a buildstep. '
|
+ 'Specify which type of test to parse, available'
|
+ ' types listed with --annotate=list.')
|
+ option_parser.add_option('--parse-input', default='',
|
+ help='When combined with --annotate, reads test '
|
+ 'from a file instead of executing a test '
|
+ 'binary. Use - for stdin.')
|
+ option_parser.add_option('--parse-result', default=0,
|
+ help='Sets the return value of the simulated '
|
+ 'executable under test. Only has meaning when '
|
+ '--parse-input is used.')
|
+ option_parser.add_option('--results-url', default='',
|
+ help='The URI of the perf dashboard to upload '
|
+ 'results to.')
|
+ option_parser.add_option('--perf-dashboard-id', default='',
|
+ help='The ID on the perf dashboard to add results '
|
+ 'to.')
|
+ option_parser.add_option('--perf-id', default='',
|
+ help='The perf builder id')
|
+ option_parser.add_option('--perf-config', default='',
|
+ help='Perf configuration dictionary (as a string). '
|
+ 'This allows to specify custom revisions to be '
|
+ 'the main revision at the Perf dashboard. '
|
+ 'Example: --perf-config="{\'a_default_rev\': '
|
+ '\'r_webrtc_rev\'}"')
|
+ option_parser.add_option('--supplemental-columns-file',
|
+ default='supplemental_columns',
|
+ help='A file containing a JSON blob with a dict '
|
+ 'that will be uploaded to the results '
|
+ 'dashboard as supplemental columns.')
|
+ option_parser.add_option('--revision',
|
+ help='The revision number which will be is used as '
|
+ 'primary key by the dashboard. If omitted it '
|
+ 'is automatically extracted from the checkout.')
|
+ option_parser.add_option('--webkit-revision',
|
+ help='See --revision.')
|
+ option_parser.add_option('--enable-asan', action='store_true', default=False,
|
+ help='Enable fast memory error detection '
|
+ '(AddressSanitizer).')
|
+ option_parser.add_option('--enable-lsan', action='store_true', default=False,
|
+ help='Enable memory leak detection (LeakSanitizer).')
|
+ option_parser.add_option('--enable-msan', action='store_true', default=False,
|
+ help='Enable uninitialized memory reads detection '
|
+ '(MemorySanitizer).')
|
+ option_parser.add_option('--enable-tsan', action='store_true', default=False,
|
+ help='Enable data race detection '
|
+ '(ThreadSanitizer).')
|
+ option_parser.add_option('--strip-path-prefix',
|
+ default='build/src/out/Release/../../',
|
+ help='Source paths in stack traces will be stripped '
|
+ 'of prefixes ending with this substring. This '
|
+ 'option is used by sanitizer tools.')
|
+ option_parser.add_option('--no-spawn-dbus', action='store_true',
|
+ default=False,
|
+ help='Disable GLib DBus bug workaround: '
|
+ 'manually spawning dbus-launch')
|
+ option_parser.add_option('--test-launcher-summary-output',
|
+ help='Path to test results file with all the info '
|
+ 'from the test launcher')
|
+ option_parser.add_option('--flakiness-dashboard-server',
|
+ help='The flakiness dashboard server to which the '
|
+ 'results should be uploaded.')
|
+ option_parser.add_option('--verbose', action='store_true', default=False,
|
+ help='Prints more information.')
|
+
|
+ chromium_utils.AddPropertiesOptions(option_parser)
|
+ options, args = option_parser.parse_args()
|
+
|
+ # Initialize logging.
|
+ log_level = logging.INFO
|
+ if options.verbose:
|
+ log_level = logging.DEBUG
|
+ logging.basicConfig(level=log_level,
|
+ format='%(asctime)s %(filename)s:%(lineno)-3d'
|
+ ' %(levelname)s %(message)s',
|
+ datefmt='%y%m%d %H:%M:%S')
|
+ logging.basicConfig(level=logging.DEBUG)
|
+ logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
|
+
|
+ if not options.perf_dashboard_id:
|
+ options.perf_dashboard_id = options.factory_properties.get('test_name')
|
+
|
+ options.test_type = options.test_type or options.factory_properties.get(
|
+ 'step_name', '')
|
+
|
+ if options.run_shell_script and options.run_python_script:
|
+ sys.stderr.write('Use either --run-shell-script OR --run-python-script, '
|
+ 'not both.')
|
+ return 1
|
+
|
+ print '[Running on builder: "%s"]' % options.builder_name
|
+
|
+ did_launch_dbus = False
|
+ if not options.no_spawn_dbus:
|
+ did_launch_dbus = _LaunchDBus()
|
+
|
+ try:
|
+ options.build_dir = build_directory.GetBuildOutputDirectory()
|
+
|
+ if options.pass_target and options.target:
|
+ args.extend(['--target', options.target])
|
+ if options.pass_build_dir:
|
+ args.extend(['--build-dir', options.build_dir])
|
+
|
+ # We will use this to accumulate overrides for the command under test,
|
+ # That we may not need or want for other support commands.
|
+ extra_env = {}
|
+
|
+ # This option is used by sanitizer code. There is no corresponding command
|
+ # line flag.
|
+ options.use_symbolization_script = False
|
+ # Set up extra environment and args for sanitizer tools.
|
+ _ConfigureSanitizerTools(options, args, extra_env)
|
+
|
+ # Set the number of shards environment variables.
|
+ # NOTE: Chromium's test launcher will ignore these in favor of the command
|
+ # line flags passed in _BuildTestBinaryCommand.
|
+ if options.total_shards and options.shard_index:
|
+ extra_env['GTEST_TOTAL_SHARDS'] = str(options.total_shards)
|
+ extra_env['GTEST_SHARD_INDEX'] = str(options.shard_index - 1)
|
+
|
+ # If perf config is passed via command line, parse the string into a dict.
|
+ if options.perf_config:
|
+ try:
|
+ options.perf_config = ast.literal_eval(options.perf_config)
|
+ assert type(options.perf_config) is dict, (
|
+ 'Value of --perf-config couldn\'t be evaluated into a dict.')
|
+ except (exceptions.SyntaxError, ValueError):
|
+ option_parser.error('Failed to parse --perf-config value into a dict: '
|
+ '%s' % options.perf_config)
|
+ return 1
|
+
|
+ # Allow factory property 'perf_config' as well during a transition period.
|
+ options.perf_config = (options.perf_config or
|
+ options.factory_properties.get('perf_config'))
|
+
|
+ if options.results_directory:
|
+ options.test_output_xml = os.path.normpath(os.path.abspath(os.path.join(
|
+ options.results_directory, '%s.xml' % options.test_type)))
|
+ args.append('--gtest_output=xml:' + options.test_output_xml)
|
+ elif options.generate_json_file:
|
+ option_parser.error(
|
+ '--results-directory is required with --generate-json-file=True')
|
+ return 1
|
+
|
+ if options.factory_properties.get('coverage_gtest_exclusions', False):
|
+ _BuildCoverageGtestExclusions(options, args)
|
+
|
+ temp_files = _GetTempCount()
|
+ if options.parse_input:
|
+ result = _MainParse(options, args)
|
+ elif sys.platform.startswith('darwin'):
|
+ test_platform = options.factory_properties.get(
|
+ 'test_platform', options.test_platform)
|
+ if test_platform in ('ios-simulator',):
|
+ result = _MainIOS(options, args, extra_env)
|
+ else:
|
+ result = _MainMac(options, args, extra_env)
|
+ elif sys.platform == 'win32':
|
+ result = _MainWin(options, args, extra_env)
|
+ elif sys.platform == 'linux2':
|
+ if options.factory_properties.get('test_platform',
|
+ options.test_platform) == 'android':
|
+ result = _MainAndroid(options, args, extra_env)
|
+ else:
|
+ result = _MainLinux(options, args, extra_env)
|
+ else:
|
+ sys.stderr.write('Unknown sys.platform value %s\n' % repr(sys.platform))
|
+ return 1
|
+
|
+ _UploadProfilingData(options, args)
|
+
|
+ new_temp_files = _GetTempCount()
|
+ if temp_files > new_temp_files:
|
+ print >> sys.stderr, (
|
+ 'Confused: %d files were deleted from %s during the test run') % (
|
+ (temp_files - new_temp_files), tempfile.gettempdir())
|
+ elif temp_files < new_temp_files:
|
+ print >> sys.stderr, (
|
+ '%d new files were left in %s: Fix the tests to clean up themselves.'
|
+ ) % ((new_temp_files - temp_files), tempfile.gettempdir())
|
+ # TODO(maruel): Make it an error soon. Not yet since I want to iron
|
+ # out all the remaining cases before.
|
+ #result = 1
|
+ return result
|
+ finally:
|
+ if did_launch_dbus:
|
+ # It looks like the command line argument --exit-with-session
|
+ # isn't working to clean up the spawned dbus-daemon. Kill it
|
+ # manually.
|
+ _ShutdownDBus()
|
+
|
+
|
+if '__main__' == __name__:
|
+ sys.exit(main())
|
|