summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/build/android/test_runner.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/libwebrtc/build/android/test_runner.py')
-rwxr-xr-xthird_party/libwebrtc/build/android/test_runner.py1194
1 files changed, 1194 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/android/test_runner.py b/third_party/libwebrtc/build/android/test_runner.py
new file mode 100755
index 0000000000..1e312ba0b8
--- /dev/null
+++ b/third_party/libwebrtc/build/android/test_runner.py
@@ -0,0 +1,1194 @@
+#!/usr/bin/env vpython3
+#
+# Copyright 2013 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.
+
+"""Runs all types of tests from one unified interface."""
+
+from __future__ import absolute_import
+import argparse
+import collections
+import contextlib
+import itertools
+import logging
+import os
+import re
+import shutil
+import signal
+import sys
+import tempfile
+import threading
+import traceback
+import unittest
+
+# Import _strptime before threaded code. datetime.datetime.strptime is
+# threadsafe except for the initial import of the _strptime module.
+# See http://crbug.com/724524 and https://bugs.python.org/issue7980.
+import _strptime # pylint: disable=unused-import
+
+# pylint: disable=ungrouped-imports
+from pylib.constants import host_paths
+
+if host_paths.DEVIL_PATH not in sys.path:
+ sys.path.append(host_paths.DEVIL_PATH)
+
+from devil import base_error
+from devil.utils import reraiser_thread
+from devil.utils import run_tests_helper
+
+from pylib import constants
+from pylib.base import base_test_result
+from pylib.base import environment_factory
+from pylib.base import output_manager
+from pylib.base import output_manager_factory
+from pylib.base import test_instance_factory
+from pylib.base import test_run_factory
+from pylib.results import json_results
+from pylib.results import report_results
+from pylib.results.presentation import test_results_presentation
+from pylib.utils import local_utils
+from pylib.utils import logdog_helper
+from pylib.utils import logging_utils
+from pylib.utils import test_filter
+
+from py_utils import contextlib_ext
+
+from lib.results import result_sink # pylint: disable=import-error
+
+_DEVIL_STATIC_CONFIG_FILE = os.path.abspath(os.path.join(
+ host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'devil_config.json'))
+
+
+def _RealPath(arg):
+ if arg.startswith('//'):
+ arg = os.path.abspath(os.path.join(host_paths.DIR_SOURCE_ROOT,
+ arg[2:].replace('/', os.sep)))
+ return os.path.realpath(arg)
+
+
+def AddTestLauncherOptions(parser):
+ """Adds arguments mirroring //base/test/launcher.
+
+ Args:
+ parser: The parser to which arguments should be added.
+ Returns:
+ The given parser.
+ """
+ parser.add_argument(
+ '--test-launcher-retry-limit',
+ '--test_launcher_retry_limit',
+ '--num_retries', '--num-retries',
+ '--isolated-script-test-launcher-retry-limit',
+ dest='num_retries', type=int, default=2,
+ help='Number of retries for a test before '
+ 'giving up (default: %(default)s).')
+ parser.add_argument(
+ '--test-launcher-summary-output',
+ '--json-results-file',
+ dest='json_results_file', type=os.path.realpath,
+ help='If set, will dump results in JSON form to the specified file. '
+ 'Note that this will also trigger saving per-test logcats to '
+ 'logdog.')
+ parser.add_argument(
+ '--test-launcher-shard-index',
+ type=int, default=os.environ.get('GTEST_SHARD_INDEX', 0),
+ help='Index of the external shard to run.')
+ parser.add_argument(
+ '--test-launcher-total-shards',
+ type=int, default=os.environ.get('GTEST_TOTAL_SHARDS', 1),
+ help='Total number of external shards.')
+
+ test_filter.AddFilterOptions(parser)
+
+ return parser
+
+
+def AddCommandLineOptions(parser):
+ """Adds arguments to support passing command-line flags to the device."""
+ parser.add_argument(
+ '--device-flags-file',
+ type=os.path.realpath,
+ help='The relative filepath to a file containing '
+ 'command-line flags to set on the device')
+ parser.add_argument(
+ '--use-apk-under-test-flags-file',
+ action='store_true',
+ help='Wether to use the flags file for the apk under test. If set, '
+ "the filename will be looked up in the APK's PackageInfo.")
+ parser.set_defaults(allow_unknown=True)
+ parser.set_defaults(command_line_flags=None)
+
+
+def AddTracingOptions(parser):
+ # TODO(shenghuazhang): Move this into AddCommonOptions once it's supported
+ # for all test types.
+ parser.add_argument(
+ '--trace-output',
+ metavar='FILENAME', type=os.path.realpath,
+ help='Path to save test_runner trace json output to.')
+
+ parser.add_argument(
+ '--trace-all',
+ action='store_true',
+ help='Whether to trace all function calls.')
+
+
+def AddCommonOptions(parser):
+ """Adds all common options to |parser|."""
+
+ default_build_type = os.environ.get('BUILDTYPE', 'Debug')
+
+ debug_or_release_group = parser.add_mutually_exclusive_group()
+ debug_or_release_group.add_argument(
+ '--debug',
+ action='store_const', const='Debug', dest='build_type',
+ default=default_build_type,
+ help='If set, run test suites under out/Debug. '
+ 'Default is env var BUILDTYPE or Debug.')
+ debug_or_release_group.add_argument(
+ '--release',
+ action='store_const', const='Release', dest='build_type',
+ help='If set, run test suites under out/Release. '
+ 'Default is env var BUILDTYPE or Debug.')
+
+ parser.add_argument(
+ '--break-on-failure', '--break_on_failure',
+ dest='break_on_failure', action='store_true',
+ help='Whether to break on failure.')
+
+ # TODO(jbudorick): Remove this once everything has switched to platform
+ # mode.
+ parser.add_argument(
+ '--enable-platform-mode',
+ action='store_true',
+ help='Run the test scripts in platform mode, which '
+ 'conceptually separates the test runner from the '
+ '"device" (local or remote, real or emulated) on '
+ 'which the tests are running. [experimental]')
+
+ parser.add_argument(
+ '-e', '--environment',
+ default='local', choices=constants.VALID_ENVIRONMENTS,
+ help='Test environment to run in (default: %(default)s).')
+
+ parser.add_argument(
+ '--local-output',
+ action='store_true',
+ help='Whether to archive test output locally and generate '
+ 'a local results detail page.')
+
+ class FastLocalDevAction(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ namespace.enable_concurrent_adb = True
+ namespace.enable_device_cache = True
+ namespace.extract_test_list_from_filter = True
+ namespace.local_output = True
+ namespace.num_retries = 0
+ namespace.skip_clear_data = True
+
+ parser.add_argument(
+ '--fast-local-dev',
+ type=bool,
+ nargs=0,
+ action=FastLocalDevAction,
+ help='Alias for: --num-retries=0 --enable-device-cache '
+ '--enable-concurrent-adb --skip-clear-data '
+ '--extract-test-list-from-filter --local-output')
+
+ # TODO(jbudorick): Remove this once downstream bots have switched to
+ # api.test_results.
+ parser.add_argument(
+ '--flakiness-dashboard-server',
+ dest='flakiness_dashboard_server',
+ help=argparse.SUPPRESS)
+ parser.add_argument(
+ '--gs-results-bucket',
+ help='Google Storage bucket to upload results to.')
+
+ parser.add_argument(
+ '--output-directory',
+ dest='output_directory', type=os.path.realpath,
+ help='Path to the directory in which build files are'
+ ' located (must include build type). This will take'
+ ' precedence over --debug and --release')
+ parser.add_argument(
+ '-v', '--verbose',
+ dest='verbose_count', default=0, action='count',
+ help='Verbose level (multiple times for more)')
+
+ parser.add_argument(
+ '--repeat', '--gtest_repeat', '--gtest-repeat',
+ '--isolated-script-test-repeat',
+ dest='repeat', type=int, default=0,
+ help='Number of times to repeat the specified set of tests.')
+
+ # This is currently only implemented for gtests and instrumentation tests.
+ parser.add_argument(
+ '--gtest_also_run_disabled_tests', '--gtest-also-run-disabled-tests',
+ '--isolated-script-test-also-run-disabled-tests',
+ dest='run_disabled', action='store_true',
+ help='Also run disabled tests if applicable.')
+
+ # These are currently only implemented for gtests.
+ parser.add_argument('--isolated-script-test-output',
+ help='If present, store test results on this path.')
+ parser.add_argument('--isolated-script-test-perf-output',
+ help='If present, store chartjson results on this path.')
+
+ AddTestLauncherOptions(parser)
+
+
+def ProcessCommonOptions(args):
+ """Processes and handles all common options."""
+ run_tests_helper.SetLogLevel(args.verbose_count, add_handler=False)
+ # pylint: disable=redefined-variable-type
+ if args.verbose_count > 0:
+ handler = logging_utils.ColorStreamHandler()
+ else:
+ handler = logging.StreamHandler(sys.stdout)
+ # pylint: enable=redefined-variable-type
+ handler.setFormatter(run_tests_helper.CustomFormatter())
+ logging.getLogger().addHandler(handler)
+
+ constants.SetBuildType(args.build_type)
+ if args.output_directory:
+ constants.SetOutputDirectory(args.output_directory)
+
+
+def AddDeviceOptions(parser):
+ """Adds device options to |parser|."""
+
+ parser = parser.add_argument_group('device arguments')
+
+ parser.add_argument(
+ '--adb-path',
+ type=os.path.realpath,
+ help='Specify the absolute path of the adb binary that '
+ 'should be used.')
+ parser.add_argument('--denylist-file',
+ type=os.path.realpath,
+ help='Device denylist file.')
+ parser.add_argument(
+ '-d', '--device', nargs='+',
+ dest='test_devices',
+ help='Target device(s) for the test suite to run on.')
+ parser.add_argument(
+ '--enable-concurrent-adb',
+ action='store_true',
+ help='Run multiple adb commands at the same time, even '
+ 'for the same device.')
+ parser.add_argument(
+ '--enable-device-cache',
+ action='store_true',
+ help='Cache device state to disk between runs')
+ parser.add_argument(
+ '--skip-clear-data',
+ action='store_true',
+ help='Do not wipe app data between tests. Use this to '
+ 'speed up local development and never on bots '
+ '(increases flakiness)')
+ parser.add_argument(
+ '--recover-devices',
+ action='store_true',
+ help='Attempt to recover devices prior to the final retry. Warning: '
+ 'this will cause all devices to reboot.')
+ parser.add_argument(
+ '--tool',
+ dest='tool',
+ help='Run the test under a tool '
+ '(use --tool help to list them)')
+
+ parser.add_argument(
+ '--upload-logcats-file',
+ action='store_true',
+ dest='upload_logcats_file',
+ help='Whether to upload logcat file to logdog.')
+
+ logcat_output_group = parser.add_mutually_exclusive_group()
+ logcat_output_group.add_argument(
+ '--logcat-output-dir', type=os.path.realpath,
+ help='If set, will dump logcats recorded during test run to directory. '
+ 'File names will be the device ids with timestamps.')
+ logcat_output_group.add_argument(
+ '--logcat-output-file', type=os.path.realpath,
+ help='If set, will merge logcats recorded during test run and dump them '
+ 'to the specified file.')
+
+
+def AddEmulatorOptions(parser):
+ """Adds emulator-specific options to |parser|."""
+ parser = parser.add_argument_group('emulator arguments')
+
+ parser.add_argument(
+ '--avd-config',
+ type=os.path.realpath,
+ help='Path to the avd config textpb. '
+ '(See //tools/android/avd/proto/ for message definition'
+ ' and existing textpb files.)')
+ parser.add_argument(
+ '--emulator-count',
+ type=int,
+ default=1,
+ help='Number of emulators to use.')
+ parser.add_argument(
+ '--emulator-window',
+ action='store_true',
+ default=False,
+ help='Enable graphical window display on the emulator.')
+
+
+def AddGTestOptions(parser):
+ """Adds gtest options to |parser|."""
+
+ parser = parser.add_argument_group('gtest arguments')
+
+ parser.add_argument(
+ '--app-data-file',
+ action='append', dest='app_data_files',
+ help='A file path relative to the app data directory '
+ 'that should be saved to the host.')
+ parser.add_argument(
+ '--app-data-file-dir',
+ help='Host directory to which app data files will be'
+ ' saved. Used with --app-data-file.')
+ parser.add_argument(
+ '--enable-xml-result-parsing',
+ action='store_true', help=argparse.SUPPRESS)
+ parser.add_argument(
+ '--executable-dist-dir',
+ type=os.path.realpath,
+ help="Path to executable's dist directory for native"
+ " (non-apk) tests.")
+ parser.add_argument(
+ '--extract-test-list-from-filter',
+ action='store_true',
+ help='When a test filter is specified, and the list of '
+ 'tests can be determined from it, skip querying the '
+ 'device for the list of all tests. Speeds up local '
+ 'development, but is not safe to use on bots ('
+ 'http://crbug.com/549214')
+ parser.add_argument(
+ '--gs-test-artifacts-bucket',
+ help=('If present, test artifacts will be uploaded to this Google '
+ 'Storage bucket.'))
+ parser.add_argument(
+ '--render-test-output-dir',
+ help='If present, store rendering artifacts in this path.')
+ parser.add_argument(
+ '--runtime-deps-path',
+ dest='runtime_deps_path', type=os.path.realpath,
+ help='Runtime data dependency file from GN.')
+ parser.add_argument(
+ '-t', '--shard-timeout',
+ dest='shard_timeout', type=int, default=120,
+ help='Timeout to wait for each test (default: %(default)s).')
+ parser.add_argument(
+ '--store-tombstones',
+ dest='store_tombstones', action='store_true',
+ help='Add tombstones in results if crash.')
+ parser.add_argument(
+ '-s', '--suite',
+ dest='suite_name', nargs='+', metavar='SUITE_NAME', required=True,
+ help='Executable name of the test suite to run.')
+ parser.add_argument(
+ '--test-apk-incremental-install-json',
+ type=os.path.realpath,
+ help='Path to install json for the test apk.')
+ parser.add_argument('--test-launcher-batch-limit',
+ dest='test_launcher_batch_limit',
+ type=int,
+ help='The max number of tests to run in a shard. '
+ 'Ignores non-positive ints and those greater than '
+ 'MAX_SHARDS')
+ parser.add_argument(
+ '-w', '--wait-for-java-debugger', action='store_true',
+ help='Wait for java debugger to attach before running any application '
+ 'code. Also disables test timeouts and sets retries=0.')
+ parser.add_argument(
+ '--coverage-dir',
+ type=os.path.realpath,
+ help='Directory in which to place all generated coverage files.')
+ parser.add_argument(
+ '--use-existing-test-data',
+ action='store_true',
+ help='Do not push new files to the device, instead using existing APK '
+ 'and test data. Only use when running the same test for multiple '
+ 'iterations.')
+
+
+def AddInstrumentationTestOptions(parser):
+ """Adds Instrumentation test options to |parser|."""
+
+ parser = parser.add_argument_group('instrumentation arguments')
+
+ parser.add_argument(
+ '--additional-apk',
+ action='append', dest='additional_apks', default=[],
+ type=_RealPath,
+ help='Additional apk that must be installed on '
+ 'the device when the tests are run')
+ parser.add_argument(
+ '-A', '--annotation',
+ dest='annotation_str',
+ help='Comma-separated list of annotations. Run only tests with any of '
+ 'the given annotations. An annotation can be either a key or a '
+ 'key-values pair. A test that has no annotation is considered '
+ '"SmallTest".')
+ # TODO(jbudorick): Remove support for name-style APK specification once
+ # bots are no longer doing it.
+ parser.add_argument(
+ '--apk-under-test',
+ help='Path or name of the apk under test.')
+ parser.add_argument(
+ '--module',
+ action='append',
+ dest='modules',
+ help='Specify Android App Bundle modules to install in addition to the '
+ 'base module.')
+ parser.add_argument(
+ '--fake-module',
+ action='append',
+ dest='fake_modules',
+ help='Specify Android App Bundle modules to fake install in addition to '
+ 'the real modules.')
+ parser.add_argument(
+ '--additional-locale',
+ action='append',
+ dest='additional_locales',
+ help='Specify locales in addition to the device locale to install splits '
+ 'for when --apk-under-test is an Android App Bundle.')
+ parser.add_argument(
+ '--coverage-dir',
+ type=os.path.realpath,
+ help='Directory in which to place all generated '
+ 'Jacoco coverage files.')
+ parser.add_argument(
+ '--disable-dalvik-asserts',
+ dest='set_asserts', action='store_false', default=True,
+ help='Removes the dalvik.vm.enableassertions property')
+ parser.add_argument(
+ '--enable-java-deobfuscation',
+ action='store_true',
+ help='Deobfuscate java stack traces in test output and logcat.')
+ parser.add_argument(
+ '-E', '--exclude-annotation',
+ dest='exclude_annotation_str',
+ help='Comma-separated list of annotations. Exclude tests with these '
+ 'annotations.')
+ parser.add_argument(
+ '--enable-breakpad-dump',
+ action='store_true',
+ help='Stores any breakpad dumps till the end of the test.')
+
+ def package_replacement(arg):
+ split_arg = arg.split(',')
+ if len(split_arg) != 2:
+ raise argparse.ArgumentError(
+ arg,
+ 'Expected two comma-separated strings for --replace-system-package, '
+ 'received %d' % len(split_arg))
+ PackageReplacement = collections.namedtuple('PackageReplacement',
+ ['package', 'replacement_apk'])
+ return PackageReplacement(package=split_arg[0],
+ replacement_apk=_RealPath(split_arg[1]))
+ parser.add_argument(
+ '--replace-system-package',
+ type=package_replacement, default=None,
+ help='Specifies a system package to replace with a given APK for the '
+ 'duration of the test. Given as a comma-separated pair of strings, '
+ 'the first element being the package and the second the path to the '
+ 'replacement APK. Only supports replacing one package. Example: '
+ '--replace-system-package com.example.app,path/to/some.apk')
+ parser.add_argument(
+ '--remove-system-package',
+ default=[],
+ action='append',
+ dest='system_packages_to_remove',
+ help='Specifies a system package to remove before testing if it exists '
+ 'on the system. WARNING: THIS WILL PERMANENTLY REMOVE THE SYSTEM APP. '
+ 'Unlike --replace-system-package, the app will not be restored after '
+ 'tests are finished.')
+
+ parser.add_argument(
+ '--use-webview-provider',
+ type=_RealPath, default=None,
+ help='Use this apk as the webview provider during test. '
+ 'The original provider will be restored if possible, '
+ "on Nougat the provider can't be determined and so "
+ 'the system will choose the default provider.')
+ parser.add_argument(
+ '--runtime-deps-path',
+ dest='runtime_deps_path', type=os.path.realpath,
+ help='Runtime data dependency file from GN.')
+ parser.add_argument(
+ '--screenshot-directory',
+ dest='screenshot_dir', type=os.path.realpath,
+ help='Capture screenshots of test failures')
+ parser.add_argument(
+ '--shared-prefs-file',
+ dest='shared_prefs_file', type=_RealPath,
+ help='The relative path to a file containing JSON list of shared '
+ 'preference files to edit and how to do so. Example list: '
+ '[{'
+ ' "package": "com.package.example",'
+ ' "filename": "ExampleSettings.xml",'
+ ' "set": {'
+ ' "boolean_key_in_xml": true,'
+ ' "string_key_in_xml": "string_value"'
+ ' },'
+ ' "remove": ['
+ ' "key_in_xml_to_remove"'
+ ' ]'
+ '}]')
+ parser.add_argument(
+ '--store-tombstones',
+ action='store_true', dest='store_tombstones',
+ help='Add tombstones in results if crash.')
+ parser.add_argument(
+ '--strict-mode',
+ dest='strict_mode', default='testing',
+ help='StrictMode command-line flag set on the device, '
+ 'death/testing to kill the process, off to stop '
+ 'checking, flash to flash only. (default: %(default)s)')
+ parser.add_argument(
+ '--test-apk',
+ required=True,
+ help='Path or name of the apk containing the tests.')
+ parser.add_argument(
+ '--test-jar',
+ help='Path of jar containing test java files.')
+ parser.add_argument(
+ '--test-launcher-batch-limit',
+ dest='test_launcher_batch_limit',
+ type=int,
+ help=('Not actually used for instrumentation tests, but can be used as '
+ 'a proxy for determining if the current run is a retry without '
+ 'patch.'))
+ parser.add_argument(
+ '--timeout-scale',
+ type=float,
+ help='Factor by which timeouts should be scaled.')
+ parser.add_argument(
+ '-w', '--wait-for-java-debugger', action='store_true',
+ help='Wait for java debugger to attach before running any application '
+ 'code. Also disables test timeouts and sets retries=0.')
+
+ # WPR record mode.
+ parser.add_argument('--wpr-enable-record',
+ action='store_true',
+ default=False,
+ help='If true, WPR server runs in record mode.'
+ 'otherwise, runs in replay mode.')
+
+ # These arguments are suppressed from the help text because they should
+ # only ever be specified by an intermediate script.
+ parser.add_argument(
+ '--apk-under-test-incremental-install-json',
+ help=argparse.SUPPRESS)
+ parser.add_argument(
+ '--test-apk-incremental-install-json',
+ type=os.path.realpath,
+ help=argparse.SUPPRESS)
+
+
+def AddSkiaGoldTestOptions(parser):
+ """Adds Skia Gold test options to |parser|."""
+ parser = parser.add_argument_group("Skia Gold arguments")
+ parser.add_argument(
+ '--code-review-system',
+ help='A non-default code review system to pass to pass to Gold, if '
+ 'applicable')
+ parser.add_argument(
+ '--continuous-integration-system',
+ help='A non-default continuous integration system to pass to Gold, if '
+ 'applicable')
+ parser.add_argument(
+ '--git-revision', help='The git commit currently being tested.')
+ parser.add_argument(
+ '--gerrit-issue',
+ help='The Gerrit issue this test is being run on, if applicable.')
+ parser.add_argument(
+ '--gerrit-patchset',
+ help='The Gerrit patchset this test is being run on, if applicable.')
+ parser.add_argument(
+ '--buildbucket-id',
+ help='The Buildbucket build ID that this test was triggered from, if '
+ 'applicable.')
+ local_group = parser.add_mutually_exclusive_group()
+ local_group.add_argument(
+ '--local-pixel-tests',
+ action='store_true',
+ default=None,
+ help='Specifies to run the Skia Gold pixel tests in local mode. When run '
+ 'in local mode, uploading to Gold is disabled and traditional '
+ 'generated/golden/diff images are output instead of triage links. '
+ 'Running in local mode also implies --no-luci-auth. If both this '
+ 'and --no-local-pixel-tests are left unset, the test harness will '
+ 'attempt to detect whether it is running on a workstation or not '
+ 'and set the options accordingly.')
+ local_group.add_argument(
+ '--no-local-pixel-tests',
+ action='store_false',
+ dest='local_pixel_tests',
+ help='Specifies to run the Skia Gold pixel tests in non-local (bot) '
+ 'mode. When run in this mode, data is actually uploaded to Gold and '
+ 'triage links are generated. If both this and --local-pixel-tests '
+ 'are left unset, the test harness will attempt to detect whether '
+ 'it is running on a workstation or not and set the options '
+ 'accordingly.')
+ parser.add_argument(
+ '--no-luci-auth',
+ action='store_true',
+ default=False,
+ help="Don't use the serve account provided by LUCI for authentication "
+ 'with Skia Gold, instead relying on gsutil to be pre-authenticated. '
+ 'Meant for testing locally instead of on the bots.')
+ parser.add_argument(
+ '--bypass-skia-gold-functionality',
+ action='store_true',
+ default=False,
+ help='Bypass all interaction with Skia Gold, effectively disabling the '
+ 'image comparison portion of any tests that use Gold. Only meant to be '
+ 'used in case a Gold outage occurs and cannot be fixed quickly.')
+
+
+def AddJUnitTestOptions(parser):
+ """Adds junit test options to |parser|."""
+
+ parser = parser.add_argument_group('junit arguments')
+
+ parser.add_argument(
+ '--coverage-on-the-fly',
+ action='store_true',
+ help='Generate coverage data by Jacoco on-the-fly instrumentation.')
+ parser.add_argument(
+ '--coverage-dir', type=os.path.realpath,
+ help='Directory to store coverage info.')
+ parser.add_argument(
+ '--package-filter',
+ help='Filters tests by package.')
+ parser.add_argument(
+ '--runner-filter',
+ help='Filters tests by runner class. Must be fully qualified.')
+ parser.add_argument(
+ '--shards',
+ default=-1,
+ type=int,
+ help='Number of shards to run junit tests in parallel on. Only 1 shard '
+ 'is supported when test-filter is specified. Values less than 1 will '
+ 'use auto select.')
+ parser.add_argument(
+ '-s', '--test-suite', required=True,
+ help='JUnit test suite to run.')
+ debug_group = parser.add_mutually_exclusive_group()
+ debug_group.add_argument(
+ '-w', '--wait-for-java-debugger', action='store_const', const='8701',
+ dest='debug_socket', help='Alias for --debug-socket=8701')
+ debug_group.add_argument(
+ '--debug-socket',
+ help='Wait for java debugger to attach at specified socket address '
+ 'before running any application code. Also disables test timeouts '
+ 'and sets retries=0.')
+
+ # These arguments are for Android Robolectric tests.
+ parser.add_argument(
+ '--robolectric-runtime-deps-dir',
+ help='Path to runtime deps for Robolectric.')
+ parser.add_argument(
+ '--resource-apk',
+ required=True,
+ help='Path to .ap_ containing binary resources for Robolectric.')
+
+
+def AddLinkerTestOptions(parser):
+
+ parser = parser.add_argument_group('linker arguments')
+
+ parser.add_argument(
+ '--test-apk',
+ type=os.path.realpath,
+ help='Path to the linker test APK.')
+
+
+def AddMonkeyTestOptions(parser):
+ """Adds monkey test options to |parser|."""
+
+ parser = parser.add_argument_group('monkey arguments')
+
+ parser.add_argument('--browser',
+ required=True,
+ choices=list(constants.PACKAGE_INFO.keys()),
+ metavar='BROWSER',
+ help='Browser under test.')
+ parser.add_argument(
+ '--category',
+ nargs='*', dest='categories', default=[],
+ help='A list of allowed categories. Monkey will only visit activities '
+ 'that are listed with one of the specified categories.')
+ parser.add_argument(
+ '--event-count',
+ default=10000, type=int,
+ help='Number of events to generate (default: %(default)s).')
+ parser.add_argument(
+ '--seed',
+ type=int,
+ help='Seed value for pseudo-random generator. Same seed value generates '
+ 'the same sequence of events. Seed is randomized by default.')
+ parser.add_argument(
+ '--throttle',
+ default=100, type=int,
+ help='Delay between events (ms) (default: %(default)s). ')
+
+
+def AddPythonTestOptions(parser):
+
+ parser = parser.add_argument_group('python arguments')
+
+ parser.add_argument('-s',
+ '--suite',
+ dest='suite_name',
+ metavar='SUITE_NAME',
+ choices=list(constants.PYTHON_UNIT_TEST_SUITES.keys()),
+ help='Name of the test suite to run.')
+
+
+def _CreateClassToFileNameDict(test_apk):
+ """Creates a dict mapping classes to file names from size-info apk."""
+ constants.CheckOutputDirectory()
+ test_apk_size_info = os.path.join(constants.GetOutDirectory(), 'size-info',
+ os.path.basename(test_apk) + '.jar.info')
+
+ class_to_file_dict = {}
+ # Some tests such as webview_cts_tests use a separately downloaded apk to run
+ # tests. This means the apk may not have been built by the system and hence
+ # no size info file exists.
+ if not os.path.exists(test_apk_size_info):
+ logging.debug('Apk size file not found. %s', test_apk_size_info)
+ return class_to_file_dict
+
+ with open(test_apk_size_info, 'r') as f:
+ for line in f:
+ file_class, file_name = line.rstrip().split(',', 1)
+ # Only want files that are not prebuilt.
+ if file_name.startswith('../../'):
+ class_to_file_dict[file_class] = str(
+ file_name.replace('../../', '//', 1))
+
+ return class_to_file_dict
+
+
+def _RunPythonTests(args):
+ """Subcommand of RunTestsCommand which runs python unit tests."""
+ suite_vars = constants.PYTHON_UNIT_TEST_SUITES[args.suite_name]
+ suite_path = suite_vars['path']
+ suite_test_modules = suite_vars['test_modules']
+
+ sys.path = [suite_path] + sys.path
+ try:
+ suite = unittest.TestSuite()
+ suite.addTests(unittest.defaultTestLoader.loadTestsFromName(m)
+ for m in suite_test_modules)
+ runner = unittest.TextTestRunner(verbosity=1+args.verbose_count)
+ return 0 if runner.run(suite).wasSuccessful() else 1
+ finally:
+ sys.path = sys.path[1:]
+
+
+_DEFAULT_PLATFORM_MODE_TESTS = [
+ 'gtest', 'instrumentation', 'junit', 'linker', 'monkey'
+]
+
+
+def RunTestsCommand(args, result_sink_client=None):
+ """Checks test type and dispatches to the appropriate function.
+
+ Args:
+ args: argparse.Namespace object.
+ result_sink_client: A ResultSinkClient object.
+
+ Returns:
+ Integer indicated exit code.
+
+ Raises:
+ Exception: Unknown command name passed in, or an exception from an
+ individual test runner.
+ """
+ command = args.command
+
+ ProcessCommonOptions(args)
+ logging.info('command: %s', ' '.join(sys.argv))
+ if args.enable_platform_mode or command in _DEFAULT_PLATFORM_MODE_TESTS:
+ return RunTestsInPlatformMode(args, result_sink_client)
+
+ if command == 'python':
+ return _RunPythonTests(args)
+ else:
+ raise Exception('Unknown test type.')
+
+
+_SUPPORTED_IN_PLATFORM_MODE = [
+ # TODO(jbudorick): Add support for more test types.
+ 'gtest',
+ 'instrumentation',
+ 'junit',
+ 'linker',
+ 'monkey',
+]
+
+
+def RunTestsInPlatformMode(args, result_sink_client=None):
+
+ def infra_error(message):
+ logging.fatal(message)
+ sys.exit(constants.INFRA_EXIT_CODE)
+
+ if args.command not in _SUPPORTED_IN_PLATFORM_MODE:
+ infra_error('%s is not yet supported in platform mode' % args.command)
+
+ ### Set up sigterm handler.
+
+ contexts_to_notify_on_sigterm = []
+ def unexpected_sigterm(_signum, _frame):
+ msg = [
+ 'Received SIGTERM. Shutting down.',
+ ]
+ for live_thread in threading.enumerate():
+ # pylint: disable=protected-access
+ thread_stack = ''.join(traceback.format_stack(
+ sys._current_frames()[live_thread.ident]))
+ msg.extend([
+ 'Thread "%s" (ident: %s) is currently running:' % (
+ live_thread.name, live_thread.ident),
+ thread_stack])
+
+ for context in contexts_to_notify_on_sigterm:
+ context.ReceivedSigterm()
+
+ infra_error('\n'.join(msg))
+
+ signal.signal(signal.SIGTERM, unexpected_sigterm)
+
+ ### Set up results handling.
+ # TODO(jbudorick): Rewrite results handling.
+
+ # all_raw_results is a list of lists of
+ # base_test_result.TestRunResults objects. Each instance of
+ # TestRunResults contains all test results produced by a single try,
+ # while each list of TestRunResults contains all tries in a single
+ # iteration.
+ all_raw_results = []
+
+ # all_iteration_results is a list of base_test_result.TestRunResults
+ # objects. Each instance of TestRunResults contains the last test
+ # result for each test run in that iteration.
+ all_iteration_results = []
+
+ global_results_tags = set()
+
+ json_file = tempfile.NamedTemporaryFile(delete=False)
+ json_file.close()
+
+ @contextlib.contextmanager
+ def json_finalizer():
+ try:
+ yield
+ finally:
+ if args.json_results_file and os.path.exists(json_file.name):
+ shutil.move(json_file.name, args.json_results_file)
+ elif args.isolated_script_test_output and os.path.exists(json_file.name):
+ shutil.move(json_file.name, args.isolated_script_test_output)
+ else:
+ os.remove(json_file.name)
+
+ @contextlib.contextmanager
+ def json_writer():
+ try:
+ yield
+ except Exception:
+ global_results_tags.add('UNRELIABLE_RESULTS')
+ raise
+ finally:
+ if args.isolated_script_test_output:
+ interrupted = 'UNRELIABLE_RESULTS' in global_results_tags
+ json_results.GenerateJsonTestResultFormatFile(all_raw_results,
+ interrupted,
+ json_file.name,
+ indent=2)
+ else:
+ json_results.GenerateJsonResultsFile(
+ all_raw_results,
+ json_file.name,
+ global_tags=list(global_results_tags),
+ indent=2)
+
+ test_class_to_file_name_dict = {}
+ # Test Location is only supported for instrumentation tests as it
+ # requires the size-info file.
+ if test_instance.TestType() == 'instrumentation':
+ test_class_to_file_name_dict = _CreateClassToFileNameDict(args.test_apk)
+
+ if result_sink_client:
+ for run in all_raw_results:
+ for results in run:
+ for r in results.GetAll():
+ # Matches chrome.page_info.PageInfoViewTest#testChromePage
+ match = re.search(r'^(.+\..+)#', r.GetName())
+ test_file_name = test_class_to_file_name_dict.get(
+ match.group(1)) if match else None
+ # Some tests put in non utf-8 char as part of the test
+ # which breaks uploads, so need to decode and re-encode.
+ log_decoded = r.GetLog()
+ if isinstance(log_decoded, bytes):
+ log_decoded = log_decoded.decode('utf-8', 'replace')
+ result_sink_client.Post(r.GetName(),
+ r.GetType(),
+ r.GetDuration(),
+ log_decoded.encode('utf-8'),
+ test_file_name,
+ failure_reason=r.GetFailureReason())
+
+ @contextlib.contextmanager
+ def upload_logcats_file():
+ try:
+ yield
+ finally:
+ if not args.logcat_output_file:
+ logging.critical('Cannot upload logcat file: no file specified.')
+ elif not os.path.exists(args.logcat_output_file):
+ logging.critical("Cannot upload logcat file: file doesn't exist.")
+ else:
+ with open(args.logcat_output_file) as src:
+ dst = logdog_helper.open_text('unified_logcats')
+ if dst:
+ shutil.copyfileobj(src, dst)
+ dst.close()
+ logging.critical(
+ 'Logcat: %s', logdog_helper.get_viewer_url('unified_logcats'))
+
+
+ logcats_uploader = contextlib_ext.Optional(
+ upload_logcats_file(),
+ 'upload_logcats_file' in args and args.upload_logcats_file)
+
+ ### Set up test objects.
+
+ out_manager = output_manager_factory.CreateOutputManager(args)
+ env = environment_factory.CreateEnvironment(
+ args, out_manager, infra_error)
+ test_instance = test_instance_factory.CreateTestInstance(args, infra_error)
+ test_run = test_run_factory.CreateTestRun(env, test_instance, infra_error)
+
+ contexts_to_notify_on_sigterm.append(env)
+ contexts_to_notify_on_sigterm.append(test_run)
+
+ ### Run.
+ with out_manager, json_finalizer():
+ with json_writer(), logcats_uploader, env, test_instance, test_run:
+
+ repetitions = (range(args.repeat +
+ 1) if args.repeat >= 0 else itertools.count())
+ result_counts = collections.defaultdict(
+ lambda: collections.defaultdict(int))
+ iteration_count = 0
+ for _ in repetitions:
+ # raw_results will be populated with base_test_result.TestRunResults by
+ # test_run.RunTests(). It is immediately added to all_raw_results so
+ # that in the event of an exception, all_raw_results will already have
+ # the up-to-date results and those can be written to disk.
+ raw_results = []
+ all_raw_results.append(raw_results)
+
+ test_run.RunTests(raw_results)
+ if not raw_results:
+ all_raw_results.pop()
+ continue
+
+ iteration_results = base_test_result.TestRunResults()
+ for r in reversed(raw_results):
+ iteration_results.AddTestRunResults(r)
+ all_iteration_results.append(iteration_results)
+ iteration_count += 1
+
+ for r in iteration_results.GetAll():
+ result_counts[r.GetName()][r.GetType()] += 1
+
+ report_results.LogFull(
+ results=iteration_results,
+ test_type=test_instance.TestType(),
+ test_package=test_run.TestPackage(),
+ annotation=getattr(args, 'annotations', None),
+ flakiness_server=getattr(args, 'flakiness_dashboard_server',
+ None))
+ if args.break_on_failure and not iteration_results.DidRunPass():
+ break
+
+ if iteration_count > 1:
+ # display summary results
+ # only display results for a test if at least one test did not pass
+ all_pass = 0
+ tot_tests = 0
+ for test_name in result_counts:
+ tot_tests += 1
+ if any(result_counts[test_name][x] for x in (
+ base_test_result.ResultType.FAIL,
+ base_test_result.ResultType.CRASH,
+ base_test_result.ResultType.TIMEOUT,
+ base_test_result.ResultType.UNKNOWN)):
+ logging.critical(
+ '%s: %s',
+ test_name,
+ ', '.join('%s %s' % (str(result_counts[test_name][i]), i)
+ for i in base_test_result.ResultType.GetTypes()))
+ else:
+ all_pass += 1
+
+ logging.critical('%s of %s tests passed in all %s runs',
+ str(all_pass),
+ str(tot_tests),
+ str(iteration_count))
+
+ if (args.local_output or not local_utils.IsOnSwarming()
+ ) and not args.isolated_script_test_output:
+ with out_manager.ArchivedTempfile(
+ 'test_results_presentation.html',
+ 'test_results_presentation',
+ output_manager.Datatype.HTML) as results_detail_file:
+ result_html_string, _, _ = test_results_presentation.result_details(
+ json_path=json_file.name,
+ test_name=args.command,
+ cs_base_url='http://cs.chromium.org',
+ local_output=True)
+ results_detail_file.write(result_html_string.encode('utf-8'))
+ results_detail_file.flush()
+ logging.critical('TEST RESULTS: %s', results_detail_file.Link())
+
+ ui_screenshots = test_results_presentation.ui_screenshot_set(
+ json_file.name)
+ if ui_screenshots:
+ with out_manager.ArchivedTempfile(
+ 'ui_screenshots.json',
+ 'ui_capture',
+ output_manager.Datatype.JSON) as ui_screenshot_file:
+ ui_screenshot_file.write(ui_screenshots)
+ logging.critical('UI Screenshots: %s', ui_screenshot_file.Link())
+
+ return (0 if all(r.DidRunPass() for r in all_iteration_results)
+ else constants.ERROR_EXIT_CODE)
+
+
+def DumpThreadStacks(_signal, _frame):
+ for thread in threading.enumerate():
+ reraiser_thread.LogThreadStack(thread)
+
+
+def main():
+ signal.signal(signal.SIGUSR1, DumpThreadStacks)
+
+ parser = argparse.ArgumentParser()
+ command_parsers = parser.add_subparsers(
+ title='test types', dest='command')
+
+ subp = command_parsers.add_parser(
+ 'gtest',
+ help='googletest-based C++ tests')
+ AddCommonOptions(subp)
+ AddDeviceOptions(subp)
+ AddEmulatorOptions(subp)
+ AddGTestOptions(subp)
+ AddTracingOptions(subp)
+ AddCommandLineOptions(subp)
+
+ subp = command_parsers.add_parser(
+ 'instrumentation',
+ help='InstrumentationTestCase-based Java tests')
+ AddCommonOptions(subp)
+ AddDeviceOptions(subp)
+ AddEmulatorOptions(subp)
+ AddInstrumentationTestOptions(subp)
+ AddSkiaGoldTestOptions(subp)
+ AddTracingOptions(subp)
+ AddCommandLineOptions(subp)
+
+ subp = command_parsers.add_parser(
+ 'junit',
+ help='JUnit4-based Java tests')
+ AddCommonOptions(subp)
+ AddJUnitTestOptions(subp)
+
+ subp = command_parsers.add_parser(
+ 'linker',
+ help='linker tests')
+ AddCommonOptions(subp)
+ AddDeviceOptions(subp)
+ AddEmulatorOptions(subp)
+ AddLinkerTestOptions(subp)
+
+ subp = command_parsers.add_parser(
+ 'monkey',
+ help="tests based on Android's monkey command")
+ AddCommonOptions(subp)
+ AddDeviceOptions(subp)
+ AddEmulatorOptions(subp)
+ AddMonkeyTestOptions(subp)
+
+ subp = command_parsers.add_parser(
+ 'python',
+ help='python tests based on unittest.TestCase')
+ AddCommonOptions(subp)
+ AddPythonTestOptions(subp)
+
+ args, unknown_args = parser.parse_known_args()
+ if unknown_args:
+ if hasattr(args, 'allow_unknown') and args.allow_unknown:
+ args.command_line_flags = unknown_args
+ else:
+ parser.error('unrecognized arguments: %s' % ' '.join(unknown_args))
+
+ # --replace-system-package/--remove-system-package has the potential to cause
+ # issues if --enable-concurrent-adb is set, so disallow that combination.
+ concurrent_adb_enabled = (hasattr(args, 'enable_concurrent_adb')
+ and args.enable_concurrent_adb)
+ replacing_system_packages = (hasattr(args, 'replace_system_package')
+ and args.replace_system_package)
+ removing_system_packages = (hasattr(args, 'system_packages_to_remove')
+ and args.system_packages_to_remove)
+ if (concurrent_adb_enabled
+ and (replacing_system_packages or removing_system_packages)):
+ parser.error('--enable-concurrent-adb cannot be used with either '
+ '--replace-system-package or --remove-system-package')
+
+ # --use-webview-provider has the potential to cause issues if
+ # --enable-concurrent-adb is set, so disallow that combination
+ if (hasattr(args, 'use_webview_provider') and
+ hasattr(args, 'enable_concurrent_adb') and args.use_webview_provider and
+ args.enable_concurrent_adb):
+ parser.error('--use-webview-provider and --enable-concurrent-adb cannot '
+ 'be used together')
+
+ if (getattr(args, 'coverage_on_the_fly', False)
+ and not getattr(args, 'coverage_dir', '')):
+ parser.error('--coverage-on-the-fly requires --coverage-dir')
+
+ if (hasattr(args, 'debug_socket') or
+ (hasattr(args, 'wait_for_java_debugger') and
+ args.wait_for_java_debugger)):
+ args.num_retries = 0
+
+ # Result-sink may not exist in the environment if rdb stream is not enabled.
+ result_sink_client = result_sink.TryInitClient()
+
+ try:
+ return RunTestsCommand(args, result_sink_client)
+ except base_error.BaseError as e:
+ logging.exception('Error occurred.')
+ if e.is_infra_error:
+ return constants.INFRA_EXIT_CODE
+ return constants.ERROR_EXIT_CODE
+ except: # pylint: disable=W0702
+ logging.exception('Unrecognized error occurred.')
+ return constants.ERROR_EXIT_CODE
+
+
+if __name__ == '__main__':
+ sys.exit(main())