Snap for 5735642 from 2dec0561511bd4f22d5d8a960c9c0ad1e2be4656 to sdk-release
Change-Id: Iba4e0c7540e1119604768ab1191cf877668f5d5e
diff --git a/.classpath b/.classpath
index 2f5448b..95c2966 100644
--- a/.classpath
+++ b/.classpath
@@ -1,10 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
- <classpathentry excluding="com/android/tradefed/testtype/junit4/LongevityHostRunner.java|com/android/tradefed/testtype/junit4/builder/DeviceJUnit4ClassRunnerBuilder.java" kind="src" path="src"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry excluding="Android.bp" kind="src" path="test_framework"/>
+ <classpathentry excluding="Android.bp" kind="src" path="invocation_interfaces"/>
+ <classpathentry excluding="Android.bp" kind="src" path="test_result_interfaces"/>
+ <classpathentry excluding="Android.bp" kind="src" path="clearcut_client"/>
<classpathentry kind="src" path="res"/>
<classpathentry kind="src" path="hamcrest"/>
<classpathentry kind="src" path="jline"/>
<classpathentry kind="src" path="junit"/>
+ <classpathentry excluding="Android.bp" kind="src" path="common_util"/>
+ <classpathentry kind="src" path="global_configuration"/>
+ <classpathentry excluding="Android.bp" kind="src" path="device_build_interfaces"/>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/tf-remote-client"/>
<classpathentry combineaccessrules="false" kind="src" path="/LongevityHostRunner"/>
<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
@@ -33,5 +40,6 @@
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/tools/common/google-api-services-storage/1.23.0/google-api-services-storage-v1-rev114-1.23.0.jar"/>
<classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/tools/tradefederation/core/tradefed-protos/linux_glibc_common/combined/tradefed-protos.jar"/>
<classpathentry kind="var" path="TRADEFED_ROOT/prebuilts/tools/common/m2/repository/com/google/code/gson/gson/2.8.0/gson-2.8.0.jar"/>
+ <classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/prebuilts/tools/common/m2/protobuf-java-util-prebuilt-jar/linux_glibc_common/combined/protobuf-java-util-prebuilt-jar.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/Android.bp b/Android.bp
index 2b63059..4b6d838 100644
--- a/Android.bp
+++ b/Android.bp
@@ -73,23 +73,35 @@
],
}
+// Main Target to build tradefed jar
java_library_host {
name: "tradefed",
defaults: ["tradefed_defaults"],
- srcs: [
- "src/**/*.java",
- ],
java_resource_dirs: [
"res",
],
- openjdk9: {
- javacflags: [
- "--add-modules=java.xml.bind",
- ],
- },
static_libs: [
+ "tradefed-lib-core",
+ "tradefed-test-framework",
+ ],
+ manifest: "MANIFEST.mf",
+}
+
+java_library_host {
+ name: "tradefed-lib-core",
+ defaults: ["tradefed_defaults"],
+ srcs: [
+ "src/**/*.java",
+ "global_configuration/**/*.java",
+ ],
+ static_libs: [
+ "tradefed-common-util",
+ "tradefed-clearcut-client",
+ "tradefed-result-interfaces",
+ "tradefed-device-build-interfaces",
+ "tradefed-invocation-interfaces",
+ "protobuf-java-util-prebuilt-jar",
"aoa-helper",
- "commons-compress-prebuilt",
"error_prone_annotations-2.0.18",
"google-api-java-client-min-repackaged",
"google-api-services-compute",
@@ -112,7 +124,6 @@
libs: [
"loganalysis",
],
- manifest: "MANIFEST.mf",
}
// Turn off various doclava warnings when generating
diff --git a/Android.mk b/Android.mk
index 8ebd686..25647f5 100644
--- a/Android.mk
+++ b/Android.mk
@@ -38,7 +38,7 @@
tradefed-core: tradefed atest_tradefed tradefed-contrib tf-contrib-tests script_help
.PHONY: tradefed-all
-tradefed-all: tradefed-core tradefed-tests tradefed_win verify loganalysis-tests
+tradefed-all: tradefed-core tradefed-tests tradefed_win loganalysis-tests
# ====================================
include $(CLEAR_VARS)
@@ -46,14 +46,14 @@
LOCAL_MODULE_TAGS := optional
-LOCAL_PREBUILT_EXECUTABLES := tradefed.sh tradefed_win.bat script_help.sh verify.sh run_tf_cmd.sh atest_tradefed.sh
+LOCAL_PREBUILT_EXECUTABLES := tradefed.sh tradefed_win.bat script_help.sh run_tf_cmd.sh atest_tradefed.sh
include $(BUILD_HOST_PREBUILT)
########################################################
# Zip up the built files and dist it as tradefed.zip
tradefed_dist_host_jars := tradefed tradefed-tests loganalysis loganalysis-tests tf-remote-client tradefed-contrib tf-contrib-tests
-tradefed_dist_host_exes := tradefed.sh tradefed_win.bat script_help.sh verify.sh run_tf_cmd.sh atest_tradefed.sh
+tradefed_dist_host_exes := tradefed.sh tradefed_win.bat script_help.sh run_tf_cmd.sh atest_tradefed.sh
tradefed_dist_test_apks := TradeFedUiTestApp TradeFedTestApp DeviceSetupUtil
# Generate a src:dest list of copies to perform.
diff --git a/README.md b/README.md
index 50fc1d3..dbc3667 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Trade Federation (TF / tradefed)
+# Trade Federation (TF / Tradefed)
TF is a test harness used to drive Android automated testing. It runs on test hosts
and monitors the connected devices, handling test scheduling & execution and device
@@ -15,3 +15,5 @@
More information at:
https://source.android.com/devices/tech/test_infra/tradefed/
+See more details about Tradefed Architecture at:
+https://source.android.com/devices/tech/test_infra/tradefed/architecture
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 29bcb06..5b5df84 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,6 +1,10 @@
+// Below lists the TEST_MAPPING tests to do TF integration tests to make sure
+// the expectation of setup with different target_preparers + run are still good.
{
"presubmit": [
{
+ // Instrumentation Test with Annotation Filter/PushFilePreparer/
+ // RumCommandTargetPreparer/TestFilePushSetup
"name": "HelloWorldTests",
"options": [
{
@@ -13,15 +17,19 @@
"host": true
},
{
+ // Gtest with FilePusher/ApkInstaller.
"name": "CtsPerfettoTestCases"
},
{
+ // Instrumentation Test with FilePusher/ApkInstaller/RumCommandTargetPreparer.
"name": "CtsApacheHttpLegacy27ApiSignatureTestCases"
},
{
+ // Gtest with FilePusher to push the whole testdata(libs/files).
"name": "ziparchive-tests"
},
{
+ // Instrumentation Test with Class Filter/ApkInstaller.
"name": "CtsDpiTestCases",
"options": [
{
@@ -30,6 +38,7 @@
]
},
{
+ // Jar Host with ApkInstaller.
"name": "CtsSampleHostTestCases"
}
]
diff --git a/TEST_MAPPING_README b/TEST_MAPPING_README
deleted file mode 100644
index d5cd4e7..0000000
--- a/TEST_MAPPING_README
+++ /dev/null
@@ -1,21 +0,0 @@
-Below lists the TEST_MAPPING tests to do TF integration tests to make sure
-the expectation of setup with different target_preparers + run are still good.
-
-* Gtest with FilePusher/ApkInstaller
- * CtsPerfettoTestCases
-
-* Gtest with FilePusher to push the whole testdata(libs/files).
- * ziparchive-tests
-
-* Instrumentation Test with Class Filter/ApkInstaller
- * CtsDpiTestCases
-
-* Instrumentation Test with FilePusher/ApkInstaller/RumCommandTargetPreparer
- * CtsApacheHttpLegacy27ApiSignatureTestCases
-
-* Instrumentation Test with Annotation Filter/PushFilePreparer/RumCommandTargetPreparer/
-* TestFilePushSetup
- * HelloWorldTests
-
-* Jar Host with ApkInstaller
- * CtsSampleHostTestCases
diff --git a/atest/Android.bp b/atest/Android.bp
index f93be04..98d9298 100644
--- a/atest/Android.bp
+++ b/atest/Android.bp
@@ -59,6 +59,9 @@
libs: [
"atest_proto",
],
+ data: [
+ "tools/updatedb_darwin.sh",
+ ],
// Make atest's built name to atest-dev
stem: "atest-dev",
defaults: ["atest_py2_default"],
@@ -68,10 +71,11 @@
name: "atest_module_info",
defaults: ["atest_lib_default"],
srcs: [
- "module_info.py",
+ "atest_error.py",
"atest_utils.py",
"constants.py",
- "constants_default.py"
+ "constants_default.py",
+ "module_info.py",
],
}
@@ -100,6 +104,8 @@
],
}
+// Exclude atest_updatedb_unittest due to it's a test for ATest's wrapper of updatedb, but there's
+// no updatedb binary on test server.
python_test_host {
name: "atest_unittests",
main: "atest_run_unittests.py",
@@ -108,6 +114,7 @@
"**/*.py",
],
data: [
+ "tools/updatedb_darwin.sh",
"unittest_data/**/*",
"unittest_data/**/.*",
],
@@ -115,6 +122,7 @@
"asuite_lib_test/*.py",
"proto/*_pb2.py",
"proto/__init__.py",
+ "tools/atest_updatedb_unittest.py",
],
libs: [
"py-mock",
@@ -180,6 +188,7 @@
name: "asuite_cc_client",
defaults: ["asuite_default"],
srcs: [
+ "atest_error.py",
"atest_utils.py",
"constants.py",
"constants_default.py",
diff --git a/atest/TEST_MAPPING b/atest/TEST_MAPPING
index 5ea63b3..cebe409 100644
--- a/atest/TEST_MAPPING
+++ b/atest/TEST_MAPPING
@@ -1,22 +1,29 @@
+// Below lists the TEST_MAPPING tests to do ASuite unittests to make sure
+// the expectation of ASuite are still good.
{
"presubmit": [
{
+ // Host side ATest unittests.
"name": "atest_unittests",
"host": true
},
{
+ // Host side metrics tests.
"name": "asuite_metrics_lib_tests",
"host": true
},
{
+ // Host side metrics tests with Python3.
"name": "asuite_metrics_lib_py3_tests",
"host": true
},
{
+ // Host side clearcut tests.
"name": "asuite_cc_lib_tests",
"host": true
},
{
+ // Host side clearcut tests with Python3.
"name": "asuite_cc_lib_py3_tests",
"host": true
}
diff --git a/atest/atest.py b/atest/atest.py
index 6ae9214..6baa28e 100755
--- a/atest/atest.py
+++ b/atest/atest.py
@@ -33,9 +33,11 @@
import platform
import atest_arg_parser
+import atest_error
import atest_execution_info
import atest_metrics
import atest_utils
+import bug_detector
import cli_translator
# pylint: disable=import-error
import constants
@@ -483,13 +485,19 @@
results_dir: Path for saving atest logs.
extra_args: Dict of extra args for test runners to utilize.
test_infos: A list of TestInfos.
+
+ Returns:
+ A list of test commands.
"""
+ all_run_cmds = []
for test_runner, tests in test_runner_handler.group_tests_by_test_runners(test_infos):
runner = test_runner(results_dir)
run_cmds = runner.generate_run_commands(tests, extra_args)
for run_cmd in run_cmds:
+ all_run_cmds.append(run_cmd)
print('Would run test via command: %s'
% (atest_utils.colorize(run_cmd, constants.GREEN)))
+ return all_run_cmds
def _print_testable_modules(mod_info, suite):
"""Print the testable modules for a given suite.
@@ -534,6 +542,9 @@
return constants.EXIT_CODE_SUCCESS
build_targets = set()
test_infos = set()
+ # Clear cache if user pass -c option
+ if args.clear_cache:
+ atest_utils.clean_test_info_caches(args.tests)
if _will_run_tests(args):
build_targets, test_infos = translator.translate(args)
if not test_infos:
@@ -547,8 +558,22 @@
build_targets |= test_runner_handler.get_test_runner_reqs(mod_info,
test_infos)
extra_args = get_extra_args(args)
+ if args.update_cmd_mapping or args.verify_cmd_mapping:
+ args.dry_run = True
if args.dry_run:
- _dry_run(results_dir, extra_args, test_infos)
+ args.tests.sort()
+ dry_run_cmds = _dry_run(results_dir, extra_args, test_infos)
+ if args.verify_cmd_mapping:
+ try:
+ atest_utils.handle_test_runner_cmd(' '.join(args.tests),
+ dry_run_cmds,
+ do_verification=True)
+ except atest_error.DryRunVerificationError as e:
+ atest_utils.colorful_print(str(e), constants.RED)
+ return constants.EXIT_CODE_VERIFY_FAILURE
+ if args.update_cmd_mapping:
+ atest_utils.handle_test_runner_cmd(' '.join(args.tests),
+ dry_run_cmds)
return constants.EXIT_CODE_SUCCESS
if args.detect_regression:
build_targets |= (regression_test_runner.RegressionTestRunner('')
@@ -601,6 +626,10 @@
RESULTS_DIR) as result_file:
metrics_base.MetricsBase.tool_name = constants.TOOL_NAME
EXIT_CODE = main(sys.argv[1:], RESULTS_DIR)
+ DETECTOR = bug_detector.BugDetector(sys.argv[1:], EXIT_CODE)
+ metrics.LocalDetectEvent(
+ detect_type=constants.DETECT_TYPE_BUG_DETECTED,
+ result=DETECTOR.caught_result)
metrics_utils.send_exit_event(EXIT_CODE)
if result_file:
print('Execution detail has saved in %s' % result_file.name)
diff --git a/atest/atest_arg_parser.py b/atest/atest_arg_parser.py
index 23b2f8c..2c9711f 100644
--- a/atest/atest_arg_parser.py
+++ b/atest/atest_arg_parser.py
@@ -93,6 +93,17 @@
help='Run the test completely on the host without '
'a device. (Note: running a host test that '
'requires a device with --host will fail.)')
+ # Option for updating dry-run command mapping result.
+ self.add_argument('-u', '--update-cmd-mapping', action='store_true',
+ help='Update the test command of input tests. '
+ 'Warning: result will be saved under '
+ 'tools/tradefederation/core/atest/test_data.')
+ # Option for verifying dry-run command mapping result.
+ self.add_argument('-y', '--verify-cmd-mapping', action='store_true',
+ help='Verify the test command of input tests.')
+ # Option for clearing cache of input test reference .
+ self.add_argument('-c', '--clear-cache', action='store_true',
+ help='Wipe out the test_infos cache of the test.')
# This arg actually doesn't consume anything, it's primarily used for the
# help description and creating custom_args in the NameSpace object.
self.add_argument('--', dest='custom_args', nargs='*',
diff --git a/atest/atest_error.py b/atest/atest_error.py
index 06ebf19..7ab8b5f 100644
--- a/atest/atest_error.py
+++ b/atest/atest_error.py
@@ -61,3 +61,6 @@
class XmlNotExistError(TestDiscoveryException):
"""Raised when the xml file does not exist."""
+
+class DryRunVerificationError(Exception):
+ """Base Exception if verification fail."""
diff --git a/atest/atest_utils.py b/atest/atest_utils.py
index 9e03e8e..983c935 100644
--- a/atest/atest_utils.py
+++ b/atest/atest_utils.py
@@ -18,9 +18,12 @@
from __future__ import print_function
+import hashlib
import itertools
+import json
import logging
import os
+import pickle
import re
import subprocess
import sys
@@ -30,10 +33,15 @@
except ImportError:
from urllib.request import urlopen
+import atest_error
import constants
-_MAKE_CMD = '%s/build/soong/soong_ui.bash' % os.environ.get(
- constants.ANDROID_BUILD_TOP)
+from metrics import metrics_base
+
+_MAKE_CMD = ('%s/build/soong/soong_ui.bash' %
+ os.path.relpath(os.environ.get(constants.ANDROID_BUILD_TOP,
+ os.getcwd()),
+ os.getcwd()))
BUILD_CMD = [_MAKE_CMD, '--make-mode']
_BASH_RESET_CODE = '\033[0m\n'
# Arbitrary number to limit stdout for failed runs in _run_limited_output.
@@ -45,7 +53,12 @@
# ex: [ 99% 39710/39711]
_BUILD_COMPILE_STATUS = re.compile(r'\[\s*(\d{1,3}%\s+)?\d+/\d+\]')
_BUILD_FAILURE = 'FAILED: '
-
+CMD_RESULT_PATH = os.path.join(os.environ.get(constants.ANDROID_BUILD_TOP,
+ os.getcwd()),
+ 'tools/tradefederation/core/atest/test_data',
+ 'sample_test_cmd_result.json')
+TEST_INFO_CACHE_ROOT = os.path.join(os.path.expanduser('~'), '.atest',
+ 'info_cache')
def _capture_fail_section(full_log):
"""Return the error message from the build output.
@@ -280,34 +293,26 @@
def is_external_run():
+ # TODO(b/133905312): remove this function after aidegen calling
+ # metrics_base.get_user_type directly.
"""Check is external run or not.
+ Determine the internal user by passing at least one check:
+ - whose git mail domain is from google
+ - whose hostname is from google
+ Otherwise is external user.
+
Returns:
True if this is an external run, False otherwise.
"""
- try:
- output = subprocess.check_output(['git', 'config', '--get', 'user.email'],
- universal_newlines=True)
- if output and output.strip().endswith(constants.INTERNAL_EMAIL):
- return False
- except OSError:
- # OSError can be raised when running atest_unittests on a host
- # without git being set up.
- # This happens before atest._configure_logging is called to set up
- # logging. Therefore, use print to log the error message, instead of
- # logging.debug.
- print('Unable to determine if this is an external run, git is not found.')
- except subprocess.CalledProcessError:
- print('Unable to determine if this is an external run, email is not '
- 'found in git config.')
- return True
+ return metrics_base.get_user_type() == metrics_base.EXTERNAL_USER
def print_data_collection_notice():
"""Print the data collection notice."""
anonymous = ''
user_type = 'INTERNAL'
- if is_external_run():
+ if metrics_base.get_user_type() == metrics_base.EXTERNAL_USER:
anonymous = ' anonymous'
user_type = 'EXTERNAL'
notice = (' We collect%s usage statistics in accordance with our Content '
@@ -323,3 +328,149 @@
colorful_print("Notice:", constants.RED)
colorful_print("%s" % notice, constants.GREEN)
print('==================\n')
+
+
+def handle_test_runner_cmd(input_test, test_cmds, do_verification=False,
+ result_path=CMD_RESULT_PATH):
+ """Handle the runner command of input tests.
+
+ Args:
+ input_test: A string of input tests pass to atest.
+ test_cmds: A list of strings for running input tests.
+ do_verification: A boolean to indicate the action of this method.
+ True: Do verification without updating result map and
+ raise DryRunVerificationError if verifying fails.
+ False: Update result map, if the former command is
+ different with current command, it will confirm
+ with user if they want to update or not.
+ result_path: The file path for saving result.
+ """
+ # Always sort test_cmds to make it comparable.
+ test_cmds.sort()
+ full_result_content = {}
+ if os.path.isfile(result_path):
+ with open(result_path) as json_file:
+ full_result_content = json.load(json_file)
+ former_test_cmds = full_result_content.get(input_test, [])
+ if former_test_cmds != test_cmds:
+ if do_verification:
+ raise atest_error.DryRunVerificationError('Dry run verification failed,'
+ 'former commands: %s' %
+ former_test_cmds)
+ if former_test_cmds:
+ # If former_test_cmds is different from test_cmds, ask users if they
+ # are willing to update the result.
+ print('Former cmds = %s' % former_test_cmds)
+ print('Current cmds = %s' % test_cmds)
+ try:
+ # TODO(b/137156054):
+ # Move the import statement into a method for that distutils is
+ # not a built-in lib in older python3(b/137017806). Will move it
+ # back when embedded_launcher fully supports Python3.
+ from distutils.util import strtobool
+ if not strtobool(raw_input('Do you want to update former result'
+ 'with the latest one?(Y/n)')):
+ print('SKIP updating result!!!')
+ return
+ except ValueError:
+ # Default action is updating the command result of the input_test.
+ # If the user input is unrecognizable telling yes or no,
+ # "Y" is implicitly applied.
+ pass
+ else:
+ # If current commands are the same as the formers, no need to update
+ # result.
+ return
+ full_result_content[input_test] = test_cmds
+ with open(result_path, 'w') as outfile:
+ json.dump(full_result_content, outfile, indent=0)
+ print('Save result mapping to %s' % result_path)
+
+def _get_hashed_file_name(main_file_name):
+ """Convert the input string to a md5-hashed string. If file_extension is
+ given, returns $(hashed_string).$(file_extension), otherwise
+ $(hashed_string).cache.
+
+ Args:
+ main_file_name: The input string need to be hashed.
+
+ Returns:
+ A string as hashed file name with .cache file extension.
+ """
+ hashed_fn = hashlib.md5(str(main_file_name).encode())
+ hashed_name = hashed_fn.hexdigest()
+ return hashed_name + '.cache'
+
+def get_test_info_cache_path(test_reference, cache_root=TEST_INFO_CACHE_ROOT):
+ """Get the cache path of the desired test_infos.
+
+ Args:
+ test_reference: A string of the test.
+ cache_root: Folder path where stores caches.
+
+ Returns:
+ A string of the path of test_info cache.
+ """
+ return os.path.join(cache_root,
+ _get_hashed_file_name(test_reference))
+
+def update_test_info_cache(test_reference, test_infos,
+ cache_root=TEST_INFO_CACHE_ROOT):
+ """Update cache content which stores a set of test_info objects through
+ pickle module, each test_reference will be saved as a cache file.
+
+ Args:
+ test_reference: A string referencing a test.
+ test_infos: A set of TestInfos.
+ cache_root: Folder path for saving caches.
+ """
+ if not os.path.isdir(cache_root):
+ os.makedirs(cache_root)
+ cache_path = get_test_info_cache_path(test_reference, cache_root)
+ # Save test_info to files.
+ try:
+ with open(cache_path, 'wb') as test_info_cache_file:
+ logging.debug('Saving cache %s.', cache_path)
+ pickle.dump(test_infos, test_info_cache_file)
+ except (pickle.PicklingError, TypeError, IOError) as err:
+ # Don't break anything, just log this error, maybe collect the exception
+ # by metrics in the future.
+ logging.debug('Exception raised: %s', err)
+
+def load_test_info_cache(test_reference, cache_root=TEST_INFO_CACHE_ROOT):
+ """Load cache by test_reference to a set of test_infos object.
+
+ Args:
+ test_reference: A string referencing a test.
+ cache_root: Folder path for finding caches.
+
+ Returns:
+ A list of TestInfo namedtuple if cache found, else None.
+ """
+ cache_file = get_test_info_cache_path(test_reference, cache_root)
+ if os.path.isfile(cache_file):
+ logging.debug('Loading cache %s.', cache_file)
+ try:
+ with open(cache_file, 'rb') as config_dictionary_file:
+ return pickle.load(config_dictionary_file)
+ except (pickle.UnpicklingError, EOFError, IOError) as err:
+ # Don't break anything, just log this error, maybe collect the
+ # exception by metrics in the future.
+ logging.debug('Exception raised: %s', err)
+ return None
+
+def clean_test_info_caches(tests, cache_root=TEST_INFO_CACHE_ROOT):
+ """Clean caches of input tests.
+
+ Args:
+ tests: A list of test references.
+ cache_root: Folder path for finding caches.
+ """
+ for test in tests:
+ cache_file = get_test_info_cache_path(test, cache_root)
+ if os.path.isfile(cache_file):
+ logging.debug('Removing cache: %s', cache_file)
+ try:
+ os.remove(cache_file)
+ except IOError as err:
+ logging.debug('Exception raised: %s', err)
diff --git a/atest/atest_utils_unittest.py b/atest/atest_utils_unittest.py
index 886f26b..b41f113 100755
--- a/atest/atest_utils_unittest.py
+++ b/atest/atest_utils_unittest.py
@@ -16,18 +16,37 @@
"""Unittests for atest_utils."""
+import hashlib
+import os
import subprocess
import sys
+import tempfile
import unittest
import mock
+import atest_error
import atest_utils
+import unittest_utils
+from test_finders import test_info
if sys.version_info[0] == 2:
from StringIO import StringIO
else:
from io import StringIO
+TEST_MODULE_NAME_A = 'ModuleNameA'
+TEST_RUNNER_A = 'FakeTestRunnerA'
+TEST_BUILD_TARGET_A = set(['bt1', 'bt2'])
+TEST_DATA_A = {'test_data_a_1': 'a1',
+ 'test_data_a_2': 'a2'}
+TEST_SUITE_A = 'FakeSuiteA'
+TEST_MODULE_CLASS_A = 'FAKE_MODULE_CLASS_A'
+TEST_INSTALL_LOC_A = set(['host', 'device'])
+TEST_INFO_A = test_info.TestInfo(TEST_MODULE_NAME_A, TEST_RUNNER_A,
+ TEST_BUILD_TARGET_A, TEST_DATA_A,
+ TEST_SUITE_A, TEST_MODULE_CLASS_A,
+ TEST_INSTALL_LOC_A)
+
#pylint: disable=protected-access
class AtestUtilsUnittests(unittest.TestCase):
"""Unit tests for atest_utils.py"""
@@ -193,26 +212,41 @@
self.assertEqual(capture_output.getvalue(),
green_wrap_no_highlight_string)
+ @mock.patch('socket.gethostname')
@mock.patch('subprocess.check_output')
- def test_is_external_run(self, mock_output):
+ def test_is_external_run(self, mock_output, mock_hostname):
"""Test method is_external_run."""
mock_output.return_value = ''
+ mock_hostname.return_value = ''
self.assertTrue(atest_utils.is_external_run())
+
mock_output.return_value = 'test@other.com'
+ mock_hostname.return_value = 'abc.com'
self.assertTrue(atest_utils.is_external_run())
+
+ mock_output.return_value = 'test@other.com'
+ mock_hostname.return_value = 'abc.google.com'
+ self.assertFalse(atest_utils.is_external_run())
+
+ mock_output.return_value = 'test@other.com'
+ mock_hostname.return_value = 'abc.google.def.com'
+ self.assertTrue(atest_utils.is_external_run())
+
mock_output.return_value = 'test@google.com'
self.assertFalse(atest_utils.is_external_run())
+
mock_output.side_effect = OSError()
self.assertTrue(atest_utils.is_external_run())
+
mock_output.side_effect = subprocess.CalledProcessError(1, 'cmd')
self.assertTrue(atest_utils.is_external_run())
- @mock.patch('atest_utils.is_external_run')
- def test_print_data_collection_notice(self, mock_is_external_run):
+ @mock.patch('metrics.metrics_base.get_user_type')
+ def test_print_data_collection_notice(self, mock_get_user_type):
"""Test method print_data_collection_notice."""
- # is_external_run return False.
- mock_is_external_run.return_value = True
+ # get_user_type return 1(external).
+ mock_get_user_type.return_value = 1
notice_str = ('\n==================\nNotice:\n'
' We collect anonymous usage statistics'
' in accordance with our'
@@ -228,8 +262,8 @@
uncolored_string = notice_str
self.assertEqual(capture_output.getvalue(), uncolored_string)
- # is_external_run return False.
- mock_is_external_run.return_value = False
+ # get_user_type return 0(internal).
+ mock_get_user_type.return_value = 0
notice_str = ('\n==================\nNotice:\n'
' We collect usage statistics'
' in accordance with our'
@@ -245,6 +279,94 @@
uncolored_string = notice_str
self.assertEqual(capture_output.getvalue(), uncolored_string)
+ @mock.patch('__builtin__.raw_input')
+ @mock.patch('json.load')
+ def test_update_test_runner_cmd(self, mock_json_load_data, mock_raw_input):
+ """Test method handle_test_runner_cmd without enable do_verification."""
+ former_cmd_str = 'Former cmds ='
+ write_result_str = 'Save result mapping to test_result'
+ tmp_file = tempfile.NamedTemporaryFile()
+ input_cmd = 'atest_args'
+ runner_cmds = ['cmd1', 'cmd2']
+ capture_output = StringIO()
+ sys.stdout = capture_output
+ # Previous data is empty. Should not enter strtobool.
+ # If entered, exception will be raised cause test fail.
+ mock_json_load_data.return_value = {}
+ atest_utils.handle_test_runner_cmd(input_cmd,
+ runner_cmds,
+ do_verification=False,
+ result_path=tmp_file.name)
+ sys.stdout = sys.__stdout__
+ self.assertEqual(capture_output.getvalue().find(former_cmd_str), -1)
+ # Previous data is the same as the new input. Should not enter strtobool.
+ # If entered, exception will be raised cause test fail
+ capture_output = StringIO()
+ sys.stdout = capture_output
+ mock_json_load_data.return_value = {input_cmd:runner_cmds}
+ atest_utils.handle_test_runner_cmd(input_cmd,
+ runner_cmds,
+ do_verification=False,
+ result_path=tmp_file.name)
+ sys.stdout = sys.__stdout__
+ self.assertEqual(capture_output.getvalue().find(former_cmd_str), -1)
+ self.assertEqual(capture_output.getvalue().find(write_result_str), -1)
+ # Previous data has different cmds. Should enter strtobool not update,
+ # should not find write_result_str.
+ prev_cmds = ['cmd1']
+ mock_raw_input.return_value = 'n'
+ capture_output = StringIO()
+ sys.stdout = capture_output
+ mock_json_load_data.return_value = {input_cmd:prev_cmds}
+ atest_utils.handle_test_runner_cmd(input_cmd,
+ runner_cmds,
+ do_verification=False,
+ result_path=tmp_file.name)
+ sys.stdout = sys.__stdout__
+ self.assertEqual(capture_output.getvalue().find(write_result_str), -1)
+
+ @mock.patch('json.load')
+ def test_verify_test_runner_cmd(self, mock_json_load_data):
+ """Test method handle_test_runner_cmd without enable update_result."""
+ tmp_file = tempfile.NamedTemporaryFile()
+ input_cmd = 'atest_args'
+ runner_cmds = ['cmd1', 'cmd2']
+ # Previous data is the same as the new input. Should not raise exception.
+ mock_json_load_data.return_value = {input_cmd:runner_cmds}
+ atest_utils.handle_test_runner_cmd(input_cmd,
+ runner_cmds,
+ do_verification=True,
+ result_path=tmp_file.name)
+ # Previous data has different cmds. Should enter strtobool and hit
+ # exception.
+ prev_cmds = ['cmd1']
+ mock_json_load_data.return_value = {input_cmd:prev_cmds}
+ self.assertRaises(atest_error.DryRunVerificationError,
+ atest_utils.handle_test_runner_cmd,
+ input_cmd,
+ runner_cmds,
+ do_verification=True,
+ result_path=tmp_file.name)
+
+ def test_get_test_info_cache_path(self):
+ """Test method get_test_info_cache_path."""
+ input_file_name = 'mytest_name'
+ cache_root = '/a/b/c'
+ expect_hashed_name = ('%s.cache' % hashlib.md5(str(input_file_name).
+ encode()).hexdigest())
+ self.assertEqual(os.path.join(cache_root, expect_hashed_name),
+ atest_utils.get_test_info_cache_path(input_file_name,
+ cache_root))
+
+ def test_get_and_load_cache(self):
+ """Test method update_test_info_cache and load_test_info_cache."""
+ test_reference = 'myTestRefA'
+ test_cache_dir = tempfile.mkdtemp()
+ atest_utils.update_test_info_cache(test_reference, TEST_INFO_A,
+ test_cache_dir)
+ unittest_utils.assert_equal_testinfos(
+ self, TEST_INFO_A,
+ atest_utils.load_test_info_cache(test_reference, test_cache_dir))
if __name__ == "__main__":
unittest.main()
diff --git a/atest/bug_detector.py b/atest/bug_detector.py
new file mode 100644
index 0000000..211e0b5
--- /dev/null
+++ b/atest/bug_detector.py
@@ -0,0 +1,126 @@
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Classes for bug events history
+"""
+
+import datetime
+import json
+import os
+
+import constants
+
+_META_FILE = os.path.join(os.path.expanduser('~'),
+ '.config', 'asuite', 'atest_history.json')
+_DETECT_OPTION_FILTER = ['-v', '--verbose']
+_DETECTED_SUCCESS = 1
+_DETECTED_FAIL = 0
+# constants of history key
+_LATEST_EXIT_CODE = 'latest_exit_code'
+_UPDATED_AT = 'updated_at'
+
+class BugDetector(object):
+ """Class for handling if a bug is detected by comparing test history."""
+
+ def __init__(self, argv, exit_code, history_file=None):
+ """BugDetector constructor
+
+ Args:
+ argv: A list of arguments.
+ exit_code: An integer of exit code.
+ history_file: A string of a given history file path.
+ """
+ self.detect_key = self.get_detect_key(argv)
+ self.exit_code = exit_code
+ self.file = history_file if history_file else _META_FILE
+ self.history = self.get_history()
+ self.caught_result = self.detect_bug_caught()
+ self.update_history()
+
+ def get_detect_key(self, argv):
+ """Get the key for history searching.
+
+ 1. remove '-v' in argv to argv_no_verbose
+ 2. sort the argv_no_verbose
+
+ Args:
+ argv: A list of arguments.
+
+ Returns:
+ A string of ordered command line.
+ """
+ argv_without_option = [x for x in argv if x not in _DETECT_OPTION_FILTER]
+ argv_without_option.sort()
+ return ' '.join(argv_without_option)
+
+ def get_history(self):
+ """Get a history object from a history file.
+
+ e.g.
+ {
+ "SystemUITests:.ScrimControllerTest":{
+ "latest_exit_code": 5, "updated_at": "2019-01-26T15:33:08.305026"},
+ "--host hello_world_test ":{
+ "latest_exit_code": 0, "updated_at": "2019-02-26T15:33:08.305026"},
+ }
+
+ Returns:
+ An object of loading from a history.
+ """
+ if os.path.exists(self.file):
+ with open(self.file) as json_file:
+ return json.load(json_file)
+ return {}
+
+ def detect_bug_caught(self):
+ """Detection of catching bugs.
+
+ When latest_exit_code and current exit_code are different, treat it
+ as a bug caught.
+
+ Returns:
+ A integer of detecting result, e.g.
+ 1: success
+ 0: fail
+ """
+ if not self.history:
+ return _DETECTED_FAIL
+ latest = self.history.get(self.detect_key, {})
+ if latest.get(_LATEST_EXIT_CODE, self.exit_code) == self.exit_code:
+ return _DETECTED_FAIL
+ return _DETECTED_SUCCESS
+
+ def update_history(self):
+ """Update the history file.
+
+ 1. update latest_bug result to history cache.
+ 2. trim history cache to size from oldest updated time.
+ 3. write to the file.
+ """
+ latest_bug = {
+ self.detect_key: {
+ _LATEST_EXIT_CODE: self.exit_code,
+ _UPDATED_AT: datetime.datetime.now().isoformat()
+ }
+ }
+ self.history.update(latest_bug)
+ num_history = len(self.history)
+ if num_history > constants.UPPER_LIMIT:
+ sorted_history = sorted(self.history.items(),
+ key=lambda kv: kv[1][_UPDATED_AT])
+ self.history = dict(
+ sorted_history[(num_history - constants.TRIM_TO_SIZE):])
+ with open(self.file, 'w') as outfile:
+ json.dump(self.history, outfile, indent=0)
diff --git a/atest/bug_detector_unittest.py b/atest/bug_detector_unittest.py
new file mode 100644
index 0000000..a9356fc
--- /dev/null
+++ b/atest/bug_detector_unittest.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+#
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittests for bug_detector."""
+
+import datetime
+import json
+import os
+import unittest
+import mock
+
+import bug_detector
+import constants
+import unittest_constants as uc
+
+TEST_DICT = {
+ 'test1': {
+ 'latest_exit_code': 5,
+ 'updated_at': ''
+ },
+ 'test2': {
+ 'latest_exit_code': 0,
+ 'updated_at': ''
+ }
+}
+
+class BugDetectorUnittest(unittest.TestCase):
+ """Unit test for bug_detector.py"""
+
+ def setUp(self):
+ """Set up stuff for testing."""
+ self.history_file = os.path.join(uc.TEST_DATA_DIR, 'bug_detector.json')
+ self.detector = bug_detector.BugDetector(['test1'], 5, self.history_file)
+ self._reset_history_file()
+ self.history_file2 = os.path.join(uc.TEST_DATA_DIR, 'bug_detector2.json')
+
+ def tearDown(self):
+ """Run after execution of every test"""
+ if os.path.isfile(self.history_file):
+ os.remove(self.history_file)
+ if os.path.isfile(self.history_file2):
+ os.remove(self.history_file2)
+
+ def _reset_history_file(self):
+ """Reset test history file."""
+ with open(self.history_file, 'w') as outfile:
+ json.dump(TEST_DICT, outfile)
+
+ def _make_test_file(self, file_size):
+ temp_history = {}
+ for i in range(file_size):
+ latest_bug = {
+ i: {
+ 'latest_exit_code': i,
+ 'updated_at': datetime.datetime.now().isoformat()
+ }
+ }
+ temp_history.update(latest_bug)
+ with open(self.history_file2, 'w') as outfile:
+ json.dump(temp_history, outfile, indent=0)
+
+ @mock.patch.object(bug_detector.BugDetector, 'update_history')
+ def test_get_detect_key(self, _):
+ """Test get_detect_key."""
+ # argv without -v
+ argv = ['test2', 'test1']
+ want_key = 'test1 test2'
+ dtr = bug_detector.BugDetector(argv, 0)
+ self.assertEqual(dtr.get_detect_key(argv), want_key)
+
+ # argv with -v
+ argv = ['-v', 'test2', 'test1']
+ want_key = 'test1 test2'
+ dtr = bug_detector.BugDetector(argv, 0)
+ self.assertEqual(dtr.get_detect_key(argv), want_key)
+
+ # argv with --verbose
+ argv = ['--verbose', 'test2', 'test3', 'test1']
+ want_key = 'test1 test2 test3'
+ dtr = bug_detector.BugDetector(argv, 0)
+ self.assertEqual(dtr.get_detect_key(argv), want_key)
+
+ def test_get_history(self):
+ """Test get_history."""
+ self.assertEqual(self.detector.get_history(), TEST_DICT)
+
+ @mock.patch.object(bug_detector.BugDetector, 'update_history')
+ def test_detect_bug_caught(self, _):
+ """Test detect_bug_caught."""
+ self._reset_history_file()
+ dtr = bug_detector.BugDetector(['test1'], 0, self.history_file)
+ success = 1
+ self.assertEqual(dtr.detect_bug_caught(), success)
+
+ def test_update_history(self):
+ """Test update_history."""
+ constants.UPPER_LIMIT = 10
+ constants.TRIM_TO_SIZE = 3
+
+ mock_file_size = 0
+ self._make_test_file(mock_file_size)
+ dtr = bug_detector.BugDetector(['test1'], 0, self.history_file2)
+ self.assertTrue(dtr.history.has_key('test1'))
+
+ # History is larger than constants.UPPER_LIMIT. Trim to size.
+ mock_file_size = 10
+ self._make_test_file(mock_file_size)
+ dtr = bug_detector.BugDetector(['test1'], 0, self.history_file2)
+ self.assertEqual(len(dtr.history), constants.TRIM_TO_SIZE)
+ keys = ['test1', '9', '8']
+ for key in keys:
+ self.assertTrue(dtr.history.has_key(key))
+
+ # History is not larger than constants.UPPER_LIMIT.
+ mock_file_size = 5
+ self._make_test_file(mock_file_size)
+ dtr = bug_detector.BugDetector(['test1'], 0, self.history_file2)
+ self.assertEqual(len(dtr.history), mock_file_size+1)
+ keys = ['test1', '4', '3', '2', '1', '0']
+ for key in keys:
+ self.assertTrue(dtr.history.has_key(key))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/atest/cli_translator.py b/atest/cli_translator.py
index e02a339..b56e79a 100644
--- a/atest/cli_translator.py
+++ b/atest/cli_translator.py
@@ -23,6 +23,7 @@
import json
import logging
import os
+import re
import sys
import time
@@ -39,6 +40,9 @@
TEST_MAPPING = 'TEST_MAPPING'
FUZZY_FINDER = 'FUZZY'
+# Pattern used to identify comments start with '//' or '#' in TEST_MAPPING.
+_COMMENTS_RE = re.compile(r'(?m)[\s\t]*(#|//).*|(\".*?\")')
+_COMMENTS = frozenset(['//', '#'])
#pylint: disable=no-self-use
class CLITranslator(object):
@@ -65,6 +69,7 @@
"""
self.mod_info = module_info
+ # pylint: disable=too-many-locals
def _find_test_infos(self, test, tm_test_detail):
"""Return set of TestInfos based on a given test.
@@ -88,29 +93,30 @@
# test name, so the details can be set after test_info object
# is created.
try:
- test_info = finder.find_method(finder.test_finder_instance,
- test)
+ found_test_infos = finder.find_method(
+ finder.test_finder_instance, test)
except atest_error.TestDiscoveryException as e:
find_test_err_msg = e
- if test_info:
- if tm_test_detail:
- test_info.data[constants.TI_MODULE_ARG] = (
- tm_test_detail.options)
- test_info.from_test_mapping = True
- test_info.host = tm_test_detail.host
- test_infos.add(test_info)
+ if found_test_infos:
+ for test_info in found_test_infos:
+ if tm_test_detail:
+ test_info.data[constants.TI_MODULE_ARG] = (
+ tm_test_detail.options)
+ test_info.from_test_mapping = True
+ test_info.host = tm_test_detail.host
+ test_infos.add(test_info)
test_found = True
finder_info = finder.finder_info
print("Found '%s' as %s" % (
atest_utils.colorize(test, constants.GREEN),
finder_info))
test_finders.append(finder_info)
- test_info_str = str(test_info)
+ test_info_str = ','.join([str(x) for x in found_test_infos])
break
if not test_found:
f_results = self._fuzzy_search_and_msg(test, find_test_err_msg)
if f_results:
- test_infos.add(f_results)
+ test_infos.update(f_results)
test_found = True
test_finders.append(FUZZY_FINDER)
metrics.FindTestFinishEvent(
@@ -120,6 +126,14 @@
test_reference=test,
test_finders=test_finders,
test_info=test_info_str)
+ # Cache test_infos by default except running with TEST_MAPPING which may
+ # include customized flags and they are likely to mess up other
+ # non-test_mapping tests.
+ if test_infos and not tm_test_detail:
+ atest_utils.update_test_info_cache(test, test_infos)
+ print('Test info has been cached for speeding up the next run, if '
+ 'test info need to be updated, please add -c to clean the '
+ 'old cache.')
return test_infos
def _fuzzy_search_and_msg(self, test, find_test_err_msg):
@@ -130,7 +144,7 @@
find_test_err_msg: A string of find test error message.
Returns:
- A TestInfos if found, otherwise None.
+ A list of TestInfos if found, otherwise None.
"""
print('No test found for: %s' %
atest_utils.colorize(test, constants.RED))
@@ -139,9 +153,10 @@
mod_finder = module_finder.ModuleFinder(self.mod_info)
results = mod_finder.get_fuzzy_searching_results(test)
if len(results) == 1 and self._confirm_running(results):
- test_info = mod_finder.find_test_by_module_name(results[0])
- if test_info:
- return test_info
+ found_test_infos = mod_finder.find_test_by_module_name(results[0])
+ # found_test_infos is a list with at most 1 element.
+ if found_test_infos:
+ return found_test_infos
elif len(results) > 1:
self._print_fuzzy_searching_results(results)
else:
@@ -204,6 +219,30 @@
for mod in results[:10]:
atest_utils.colorful_print(mod, constants.GREEN)
+ def filter_comments(self, test_mapping_file):
+ """Remove comments in TEST_MAPPING file to valid format. Only '//' and
+ '#' are regarded as comments.
+
+ Args:
+ test_mapping_file: Path to a TEST_MAPPING file.
+
+ Returns:
+ Valid json string without comments.
+ """
+ def _replace(match):
+ """Replace comments if found matching the defined regular expression.
+
+ Args:
+ match: The matched regex pattern
+
+ Returns:
+ "" if it matches _COMMENTS, otherwise original string.
+ """
+ line = match.group(0).strip()
+ return "" if any(map(line.startswith, _COMMENTS)) else line
+ with open(test_mapping_file) as json_file:
+ return re.sub(_COMMENTS_RE, _replace, json_file.read())
+
def _read_tests_in_test_mapping(self, test_mapping_file):
"""Read tests from a TEST_MAPPING file.
@@ -219,9 +258,7 @@
"""
all_tests = {}
imports = []
- test_mapping_dict = None
- with open(test_mapping_file) as json_file:
- test_mapping_dict = json.load(json_file)
+ test_mapping_dict = json.loads(self.filter_comments(test_mapping_file))
for test_group_name, test_list in test_mapping_dict.items():
if test_group_name == constants.TEST_MAPPING_IMPORTS:
for import_detail in test_list:
@@ -302,11 +339,7 @@
grouped_tests.update(test_list)
tests = set(merged_all_tests.get(test_group, []))
- # Postsubmit tests shall include all presubmit tests as well.
- if test_group == constants.TEST_GROUP_POSTSUBMIT:
- tests.update(merged_all_tests.get(
- constants.TEST_GROUP_PRESUBMIT, set()))
- elif test_group == constants.TEST_GROUP_ALL:
+ if test_group == constants.TEST_GROUP_ALL:
for grouped_tests in merged_all_tests.values():
tests.update(grouped_tests)
return tests, merged_all_tests, all_imports
diff --git a/atest/cli_translator_unittest.py b/atest/cli_translator_unittest.py
index 5504fc2..1b6137b 100755
--- a/atest/cli_translator_unittest.py
+++ b/atest/cli_translator_unittest.py
@@ -17,6 +17,7 @@
"""Unittests for cli_translator."""
import unittest
+import json
import os
import re
import sys
@@ -82,6 +83,8 @@
# Test mapping related args
self.args.test_mapping = False
self.args.include_subdirs = False
+ # Cache finder related args
+ self.args.clear_cache = False
self.ctr.mod_info = mock.Mock
self.ctr.mod_info.name_to_module_info = {}
@@ -99,11 +102,11 @@
mock_findtestbymodule, mock_raw_input):
"""Test _get_test_infos method."""
ctr = cli_t.CLITranslator()
- find_method_return_module_info = lambda x, y: uc.MODULE_INFO
+ find_method_return_module_info = lambda x, y: uc.MODULE_INFOS
# pylint: disable=invalid-name
- find_method_return_module_class_info = (lambda x, test: uc.MODULE_INFO
+ find_method_return_module_class_info = (lambda x, test: uc.MODULE_INFOS
if test == uc.MODULE_NAME
- else uc.CLASS_INFO)
+ else uc.CLASS_INFOS)
find_method_return_nothing = lambda x, y: None
one_test = [uc.MODULE_NAME]
mult_test = [uc.MODULE_NAME, uc.CLASS_NAME]
@@ -161,6 +164,54 @@
test_detail2.options,
test_info.data[constants.TI_MODULE_ARG])
+ @mock.patch.object(metrics, 'FindTestFinishEvent')
+ @mock.patch.object(test_finder_handler, 'get_find_methods_for_test')
+ def test_get_test_infos_2(self, mock_getfindmethods, _metrics):
+ """Test _get_test_infos method."""
+ ctr = cli_t.CLITranslator()
+ find_method_return_module_info2 = lambda x, y: uc.MODULE_INFOS2
+ find_method_ret_mod_cls_info2 = (
+ lambda x, test: uc.MODULE_INFOS2
+ if test == uc.MODULE_NAME else uc.CLASS_INFOS2)
+ one_test = [uc.MODULE_NAME]
+ mult_test = [uc.MODULE_NAME, uc.CLASS_NAME]
+ # Let's make sure we return what we expect.
+ expected_test_infos = {uc.MODULE_INFO, uc.MODULE_INFO2}
+ mock_getfindmethods.return_value = [
+ test_finder_base.Finder(None, find_method_return_module_info2,
+ None)]
+ unittest_utils.assert_strict_equal(
+ self, ctr._get_test_infos(one_test), expected_test_infos)
+ # Check we receive multiple test infos.
+ expected_test_infos = {uc.MODULE_INFO, uc.CLASS_INFO, uc.MODULE_INFO2,
+ uc.CLASS_INFO2}
+ mock_getfindmethods.return_value = [
+ test_finder_base.Finder(None, find_method_ret_mod_cls_info2,
+ None)]
+ unittest_utils.assert_strict_equal(
+ self, ctr._get_test_infos(mult_test), expected_test_infos)
+ # Check the method works for test mapping.
+ test_detail1 = test_mapping.TestDetail(uc.TEST_MAPPING_TEST)
+ test_detail2 = test_mapping.TestDetail(uc.TEST_MAPPING_TEST_WITH_OPTION)
+ expected_test_infos = {uc.MODULE_INFO, uc.CLASS_INFO, uc.MODULE_INFO2,
+ uc.CLASS_INFO2}
+ mock_getfindmethods.return_value = [
+ test_finder_base.Finder(None, find_method_ret_mod_cls_info2,
+ None)]
+ test_infos = ctr._get_test_infos(
+ mult_test, [test_detail1, test_detail2])
+ unittest_utils.assert_strict_equal(
+ self, test_infos, expected_test_infos)
+ for test_info in test_infos:
+ if test_info in [uc.MODULE_INFO, uc.MODULE_INFO2]:
+ self.assertEqual(
+ test_detail1.options,
+ test_info.data[constants.TI_MODULE_ARG])
+ elif test_info in [uc.CLASS_INFO, uc.CLASS_INFO2]:
+ self.assertEqual(
+ test_detail2.options,
+ test_info.data[constants.TI_MODULE_ARG])
+
@mock.patch.object(cli_t.CLITranslator, '_get_test_infos',
side_effect=gettestinfos_side_effect)
def test_translate_class(self, _info):
@@ -242,9 +293,7 @@
test_group=constants.TEST_GROUP_POSTSUBMIT,
file_name='test_mapping_sample', checked_files=set())
expected_presubmit = set([TEST_1, TEST_2, TEST_5, TEST_7, TEST_9])
- expected = set(
- [TEST_1, TEST_2, TEST_3, TEST_5, TEST_6, TEST_7, TEST_8, TEST_9,
- TEST_10])
+ expected = set([TEST_3, TEST_6, TEST_8, TEST_10])
expected_all_tests = {'presubmit': expected_presubmit,
'postsubmit': set(
[TEST_3, TEST_6, TEST_8, TEST_10]),
@@ -304,5 +353,22 @@
uc.MODULE_NAME, uc.MODULE2_NAME)
self.assertEquals(capture_output.getvalue(), output)
+ def test_filter_comments(self):
+ """Test filter_comments method"""
+ file_with_comments = os.path.join(TEST_MAPPING_TOP_DIR,
+ 'folder6',
+ 'test_mapping_sample_with_comments')
+ file_with_comments_golden = os.path.join(TEST_MAPPING_TOP_DIR,
+ 'folder6',
+ 'test_mapping_sample_golden')
+ test_mapping_dict = json.loads(
+ self.ctr.filter_comments(file_with_comments))
+ test_mapping_dict_gloden = None
+ with open(file_with_comments_golden) as json_file:
+ test_mapping_dict_gloden = json.load(json_file)
+
+ self.assertEqual(test_mapping_dict, test_mapping_dict_gloden)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/atest/constants_default.py b/atest/constants_default.py
index abc6740..676f993 100644
--- a/atest/constants_default.py
+++ b/atest/constants_default.py
@@ -16,6 +16,7 @@
Various globals used by atest.
"""
+import re
MODE = 'DEFAULT'
@@ -52,6 +53,7 @@
EXIT_CODE_ERROR = 3
EXIT_CODE_TEST_NOT_FOUND = 4
EXIT_CODE_TEST_FAILURE = 5
+EXIT_CODE_VERIFY_FAILURE = 6
# Test finder constants.
MODULE_CONFIG = 'AndroidTest.xml'
@@ -64,6 +66,8 @@
MODULE_CLASS_NATIVE_TESTS = 'NATIVE_TESTS'
MODULE_CLASS_JAVA_LIBRARIES = 'JAVA_LIBRARIES'
MODULE_TEST_CONFIG = 'test_config'
+CC_EXT_RE = re.compile(r'.*\.(cc|cpp)$', re.I)
+JAVA_EXT_RE = re.compile(r'.*\.(java|kt)$', re.I)
# Env constants
ANDROID_BUILD_TOP = 'ANDROID_BUILD_TOP'
@@ -134,6 +138,7 @@
EXTERNAL = 'EXTERNAL_RUN'
INTERNAL = 'INTERNAL_RUN'
INTERNAL_EMAIL = '@google.com'
+INTERNAL_HOSTNAME = '.google.com'
CONTENT_LICENSES_URL = 'https://source.android.com/setup/start/licenses'
CONTRIBUTOR_AGREEMENT_URL = {
'INTERNAL': 'https://cla.developers.google.com/',
@@ -143,6 +148,15 @@
TERMS_SERVICE_URL = 'https://policies.google.com/terms'
TOOL_NAME = 'atest'
+# Detect type for local_detect_event.
+# Next expansion : DETECT_TYPE_XXX = 1
+DETECT_TYPE_BUG_DETECTED = 0
+# Considering a trade-off between speed and size, we set UPPER_LIMIT to 100000
+# to make maximum file space 10M(100000(records)*100(byte/record)) at most.
+# Therefore, to update history file will spend 1 sec at most in each run.
+UPPER_LIMIT = 100000
+TRIM_TO_SIZE = 50000
+
# VTS plans
VTS_STAGING_PLAN = 'vts-staging-default'
diff --git a/atest/metrics/metrics_base.py b/atest/metrics/metrics_base.py
index b716092..73027b4 100644
--- a/atest/metrics/metrics_base.py
+++ b/atest/metrics/metrics_base.py
@@ -15,12 +15,16 @@
"""
Metrics base class.
"""
+
+from __future__ import print_function
+
import logging
import random
+import socket
+import subprocess
import time
import uuid
-import atest_utils
import asuite_metrics
import constants
@@ -43,6 +47,41 @@
EXTERNAL_USER: 934
}
+
+def get_user_type():
+ """Get user type.
+
+ Determine the internal user by passing at least one check:
+ - whose git mail domain is from google
+ - whose hostname is from google
+ Otherwise is external user.
+
+ Returns:
+ INTERNAL_USER if user is internal, EXTERNAL_USER otherwise.
+ """
+ try:
+ output = subprocess.check_output(['git', 'config', '--get', 'user.email'],
+ universal_newlines=True)
+ if output and output.strip().endswith(constants.INTERNAL_EMAIL):
+ return INTERNAL_USER
+ except OSError:
+ # OSError can be raised when running atest_unittests on a host
+ # without git being set up.
+ logging.debug('Unable to determine if this is an external run, git is '
+ 'not found.')
+ except subprocess.CalledProcessError:
+ logging.debug('Unable to determine if this is an external run, email '
+ 'is not found in git config.')
+ try:
+ hostname = socket.getfqdn()
+ if hostname and constants.INTERNAL_HOSTNAME in hostname:
+ return INTERNAL_USER
+ except IOError:
+ logging.debug('Unable to determine if this is an external run, '
+ 'hostname is not found.')
+ return EXTERNAL_USER
+
+
class MetricsBase(object):
"""Class for separating allowed fields and sending metric."""
@@ -53,8 +92,7 @@
#pylint: disable=broad-except
except Exception:
_user_key = asuite_metrics.DUMMY_UUID
- _user_type = (EXTERNAL_USER if atest_utils.is_external_run()
- else INTERNAL_USER)
+ _user_type = get_user_type()
_log_source = ATEST_LOG_SOURCE[_user_type]
cc = clearcut_client.Clearcut(_log_source)
tool_name = None
diff --git a/atest/test_data/sample_test_cmd_result.json b/atest/test_data/sample_test_cmd_result.json
new file mode 100644
index 0000000..9128dd1
--- /dev/null
+++ b/atest/test_data/sample_test_cmd_result.json
@@ -0,0 +1 @@
+{"hello_world_test": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter hello_world_test --log-level WARN"], "HelloWorldTests": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter HelloWorldTests --log-level WARN"], "packages/apps/Car/Messenger/tests/robotests/src/com/android/car/messenger/tts/TTSHelperTest.java": ["./build/soong/soong_ui.bash --make-mode RunCarMessengerRoboTests"], "CtsDeviceJankUi": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsJankDeviceTestCases --atest-include-filter CtsJankDeviceTestCases:android.jank.cts.ui.CtsDeviceJankUi --log-level WARN"], "CtsJankDeviceTestCases CtsSampleDeviceTestCases": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --include-filter CtsJankDeviceTestCases --log-level WARN"], "VtsCodelabHelloWorldTest": ["vts-tradefed run commandAndExit vts-staging-default -m VtsCodelabHelloWorldTest --skip-all-system-status-check --skip-preconditions --primary-abi-only"], "PacketFragmenterTest": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter net_test_hci --atest-include-filter net_test_hci:PacketFragmenterTest.* --log-level WARN"], "platform_testing/tests/example/native/Android.bp": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter hello_world_test --log-level WARN"], "android.jank.cts": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsJankDeviceTestCases --atest-include-filter CtsJankDeviceTestCases:android.jank.cts --log-level WARN"], "tools/tradefederation/core/res/config/native-benchmark.xml": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter native-benchmark --log-level WARN"], "native-benchmark": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter native-benchmark --log-level WARN"], "platform_testing/tests/example/native": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter hello_world_test --log-level WARN"], "PacketFragmenterTest#test_no_fragment_necessary,test_ble_fragment_necessary": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter net_test_hci --atest-include-filter net_test_hci:PacketFragmenterTest.test_ble_fragment_necessary:PacketFragmenterTest.test_no_fragment_necessary --log-level WARN"], "CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceReportLogTest": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceReportLogTest --log-level WARN"], "CtsSampleDeviceTestCases:SampleDeviceTest#testSharedPreferences": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceTest#testSharedPreferences --log-level WARN"], "CtsSampleDeviceTestCases:android.sample.cts": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts --log-level WARN"], "CtsJankDeviceTestCases:CtsDeviceJankUi": ["atest_tradefed.sh template/local_min --template:map test=atest --include-filter CtsJankDeviceTestCases --atest-include-filter CtsJankDeviceTestCases:android.jank.cts.ui.CtsDeviceJankUi --log-level WARN"], "CarMessengerRoboTests": ["./build/soong/soong_ui.bash --make-mode RunCarMessengerRoboTests"]}
\ No newline at end of file
diff --git a/atest/test_finder_handler.py b/atest/test_finder_handler.py
index 53ee79f..236f177 100644
--- a/atest/test_finder_handler.py
+++ b/atest/test_finder_handler.py
@@ -19,6 +19,7 @@
import logging
import atest_enum
+from test_finders import cache_finder
from test_finders import test_finder_base
from test_finders import suite_plan_finder
from test_finders import tf_integration_finder
@@ -29,6 +30,7 @@
suite_plan_finder.SuitePlanFinder,
tf_integration_finder.TFIntegrationFinder,
module_finder.ModuleFinder,
+ cache_finder.CacheFinder,
}
# Explanation of REFERENCE_TYPEs:
@@ -51,7 +53,7 @@
'MODULE_PACKAGE', 'MODULE_FILE_PATH',
'INTEGRATION_FILE_PATH', 'INTEGRATION',
'SUITE', 'CC_CLASS', 'SUITE_PLAN',
- 'SUITE_PLAN_FILE_PATH'])
+ 'SUITE_PLAN_FILE_PATH', 'CACHE'])
_REF_TYPE_TO_FUNC_MAP = {
_REFERENCE_TYPE.MODULE: module_finder.ModuleFinder.find_test_by_module_name,
@@ -70,6 +72,7 @@
_REFERENCE_TYPE.SUITE_PLAN:suite_plan_finder.SuitePlanFinder.find_test_by_suite_name,
_REFERENCE_TYPE.SUITE_PLAN_FILE_PATH:
suite_plan_finder.SuitePlanFinder.find_test_by_suite_path,
+ _REFERENCE_TYPE.CACHE: cache_finder.CacheFinder.find_test_by_cache,
}
@@ -124,15 +127,18 @@
A list of possible REFERENCE_TYPEs (ints) for reference string.
"""
if ref.startswith('.') or '..' in ref:
- return [_REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+ return [_REFERENCE_TYPE.CACHE,
+ _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
_REFERENCE_TYPE.MODULE_FILE_PATH,
_REFERENCE_TYPE.SUITE_PLAN_FILE_PATH]
if '/' in ref:
if ref.startswith('/'):
- return [_REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+ return [_REFERENCE_TYPE.CACHE,
+ _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
_REFERENCE_TYPE.MODULE_FILE_PATH,
_REFERENCE_TYPE.SUITE_PLAN_FILE_PATH]
- return [_REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+ return [_REFERENCE_TYPE.CACHE,
+ _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
_REFERENCE_TYPE.MODULE_FILE_PATH,
_REFERENCE_TYPE.INTEGRATION,
_REFERENCE_TYPE.SUITE_PLAN_FILE_PATH,
@@ -146,12 +152,14 @@
if '.' in ref:
if ref_end_is_upper:
# Module:fully.qualified.Class or Integration:fully.q.Class
- return [_REFERENCE_TYPE.INTEGRATION,
+ return [_REFERENCE_TYPE.CACHE,
+ _REFERENCE_TYPE.INTEGRATION,
_REFERENCE_TYPE.MODULE_CLASS]
# Module:some.package
- return [_REFERENCE_TYPE.MODULE_PACKAGE]
+ return [_REFERENCE_TYPE.CACHE, _REFERENCE_TYPE.MODULE_PACKAGE]
# Module:Class or IntegrationName:Class
- return [_REFERENCE_TYPE.INTEGRATION,
+ return [_REFERENCE_TYPE.CACHE,
+ _REFERENCE_TYPE.INTEGRATION,
_REFERENCE_TYPE.MODULE_CLASS]
if '.' in ref:
# The string of ref_end possibly includes specific mathods, e.g.
@@ -159,18 +167,21 @@
if "#" in ref_end:
ref_end = ref_end.split('#')[0]
if ref_end in ('java', 'kt', 'bp', 'mk', 'cc', 'cpp'):
- return [_REFERENCE_TYPE.MODULE_FILE_PATH]
+ return [_REFERENCE_TYPE.CACHE, _REFERENCE_TYPE.MODULE_FILE_PATH]
if ref_end == 'xml':
- return [_REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+ return [_REFERENCE_TYPE.CACHE,
+ _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
_REFERENCE_TYPE.SUITE_PLAN_FILE_PATH]
if ref_end_is_upper:
- return [_REFERENCE_TYPE.QUALIFIED_CLASS]
- return [_REFERENCE_TYPE.MODULE,
+ return [_REFERENCE_TYPE.CACHE, _REFERENCE_TYPE.QUALIFIED_CLASS]
+ return [_REFERENCE_TYPE.CACHE,
+ _REFERENCE_TYPE.MODULE,
_REFERENCE_TYPE.PACKAGE]
# Note: We assume that if you're referencing a file in your cwd,
# that file must have a '.' in its name, i.e. foo.java, foo.xml.
# If this ever becomes not the case, then we need to include path below.
- return [_REFERENCE_TYPE.INTEGRATION,
+ return [_REFERENCE_TYPE.CACHE,
+ _REFERENCE_TYPE.INTEGRATION,
# TODO: Comment in SUITE when it's supported
# _REFERENCE_TYPE.SUITE,
_REFERENCE_TYPE.MODULE,
diff --git a/atest/test_finder_handler_unittest.py b/atest/test_finder_handler_unittest.py
index d834d05..8f5e822 100755
--- a/atest/test_finder_handler_unittest.py
+++ b/atest/test_finder_handler_unittest.py
@@ -93,119 +93,124 @@
"""Test _get_test_reference_types parses reference types correctly."""
self.assertEqual(
test_finder_handler._get_test_reference_types('ModuleOrClassName'),
- [REF_TYPE.INTEGRATION, REF_TYPE.MODULE, REF_TYPE.SUITE_PLAN,
- REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE,
+ REF_TYPE.SUITE_PLAN, REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('Module_or_Class_name'),
- [REF_TYPE.INTEGRATION, REF_TYPE.MODULE, REF_TYPE.SUITE_PLAN,
- REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE,
+ REF_TYPE.SUITE_PLAN, REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('SuiteName'),
- [REF_TYPE.INTEGRATION, REF_TYPE.MODULE, REF_TYPE.SUITE_PLAN,
- REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE,
+ REF_TYPE.SUITE_PLAN, REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('Suite-Name'),
- [REF_TYPE.INTEGRATION, REF_TYPE.MODULE, REF_TYPE.SUITE_PLAN,
- REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE,
+ REF_TYPE.SUITE_PLAN, REF_TYPE.CLASS, REF_TYPE.CC_CLASS]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('some.package'),
- [REF_TYPE.MODULE, REF_TYPE.PACKAGE]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE, REF_TYPE.PACKAGE]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('fully.q.Class'),
- [REF_TYPE.QUALIFIED_CLASS]
+ [REF_TYPE.CACHE, REF_TYPE.QUALIFIED_CLASS]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('Integration.xml'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.SUITE_PLAN_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('SomeClass.java'),
- [REF_TYPE.MODULE_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('SomeClass.kt'),
- [REF_TYPE.MODULE_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('Android.mk'),
- [REF_TYPE.MODULE_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('Android.bp'),
- [REF_TYPE.MODULE_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('SomeTest.cc'),
- [REF_TYPE.MODULE_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('SomeTest.cpp'),
- [REF_TYPE.MODULE_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('SomeTest.cc#method'),
- [REF_TYPE.MODULE_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('module:Class'),
- [REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('module:f.q.Class'),
- [REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('module:a.package'),
- [REF_TYPE.MODULE_PACKAGE]
+ [REF_TYPE.CACHE, REF_TYPE.MODULE_PACKAGE]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('.'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
- REF_TYPE.SUITE_PLAN_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.MODULE_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('..'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
- REF_TYPE.SUITE_PLAN_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.MODULE_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('./rel/path/to/test'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
- REF_TYPE.SUITE_PLAN_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.MODULE_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('rel/path/to/test'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
- REF_TYPE.INTEGRATION, REF_TYPE.SUITE_PLAN_FILE_PATH]
- )
- self.assertEqual(
- test_finder_handler._get_test_reference_types('/abs/path/to/test'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.MODULE_FILE_PATH, REF_TYPE.INTEGRATION,
REF_TYPE.SUITE_PLAN_FILE_PATH]
)
self.assertEqual(
+ test_finder_handler._get_test_reference_types('/abs/path/to/test'),
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.MODULE_FILE_PATH, REF_TYPE.SUITE_PLAN_FILE_PATH]
+ )
+ self.assertEqual(
test_finder_handler._get_test_reference_types('int/test'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
- REF_TYPE.INTEGRATION, REF_TYPE.SUITE_PLAN_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.MODULE_FILE_PATH, REF_TYPE.INTEGRATION,
+ REF_TYPE.SUITE_PLAN_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('int/test:fully.qual.Class#m'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
- REF_TYPE.INTEGRATION, REF_TYPE.SUITE_PLAN_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.MODULE_FILE_PATH, REF_TYPE.INTEGRATION,
+ REF_TYPE.SUITE_PLAN_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('int/test:Class#method'),
- [REF_TYPE.INTEGRATION_FILE_PATH, REF_TYPE.MODULE_FILE_PATH,
- REF_TYPE.INTEGRATION, REF_TYPE.SUITE_PLAN_FILE_PATH]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION_FILE_PATH,
+ REF_TYPE.MODULE_FILE_PATH, REF_TYPE.INTEGRATION,
+ REF_TYPE.SUITE_PLAN_FILE_PATH]
)
self.assertEqual(
test_finder_handler._get_test_reference_types('int_name_no_slash:Class#m'),
- [REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
+ [REF_TYPE.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
)
def test_get_registered_find_methods(self):
diff --git a/atest/test_finders/cache_finder.py b/atest/test_finders/cache_finder.py
new file mode 100644
index 0000000..3937ef0
--- /dev/null
+++ b/atest/test_finders/cache_finder.py
@@ -0,0 +1,38 @@
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Cache Finder class.
+"""
+
+import atest_utils
+from test_finders import test_finder_base
+
+class CacheFinder(test_finder_base.TestFinderBase):
+ """Cache Finder class."""
+ NAME = 'CACHE'
+
+ def __init__(self, **kwargs):
+ super(CacheFinder, self).__init__()
+
+ def find_test_by_cache(self, test_reference):
+ """Find the matched test_infos in saved caches.
+
+ Args:
+ test_reference: A string of the path to the test's file or dir.
+
+ Returns:
+ A list of TestInfo namedtuple if cache found, else None.
+ """
+ return atest_utils.load_test_info_cache(test_reference)
diff --git a/atest/test_finders/cache_finder_unittest.py b/atest/test_finders/cache_finder_unittest.py
new file mode 100755
index 0000000..92de278
--- /dev/null
+++ b/atest/test_finders/cache_finder_unittest.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+#
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittests for cache_finder."""
+
+import unittest
+import os
+import mock
+
+# pylint: disable=import-error
+import atest_utils
+import unittest_constants as uc
+from test_finders import cache_finder
+
+
+#pylint: disable=protected-access
+class CacheFinderUnittests(unittest.TestCase):
+ """Unit tests for cache_finder.py"""
+ def setUp(self):
+ """Set up stuff for testing."""
+ self.cache_finder = cache_finder.CacheFinder()
+
+ @mock.patch.object(atest_utils, 'get_test_info_cache_path')
+ def test_find_test_by_cache(self, mock_get_cache_path):
+ """Test find_test_by_cache method."""
+ cached_test = 'mytest1'
+ uncached_test = 'mytest2'
+ test_cache_root = os.path.join(uc.TEST_DATA_DIR, 'cache_root')
+ # Hit matched cache file, should return cached test infos.
+ mock_get_cache_path.return_value = os.path.join(
+ test_cache_root,
+ 'cd66f9f5ad63b42d0d77a9334de6bb73.cache')
+ self.assertIsNotNone(self.cache_finder.find_test_by_cache(cached_test))
+ # Does not hit matched cache file, should return cached test infos.
+ mock_get_cache_path.return_value = os.path.join(
+ test_cache_root,
+ '39488b7ac83c56d5a7d285519fe3e3fd.cache')
+ self.assertIsNone(self.cache_finder.find_test_by_cache(uncached_test))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/atest/test_finders/module_finder.py b/atest/test_finders/module_finder.py
index 6c3ecd6..91b10be 100644
--- a/atest/test_finders/module_finder.py
+++ b/atest/test_finders/module_finder.py
@@ -18,7 +18,6 @@
import logging
import os
-import re
# pylint: disable=import-error
import atest_error
@@ -31,9 +30,6 @@
from test_runners import robolectric_test_runner
from test_runners import vts_tf_test_runner
-_CC_EXT_RE = re.compile(r'.*(\.cc|\.cpp)$', re.I)
-_JAVA_EXT_RE = re.compile(r'.*(\.java|\.kt)$', re.I)
-
_MODULES_IN = 'MODULES-IN-%s'
_ANDROID_MK = 'Android.mk'
@@ -63,7 +59,7 @@
path: String path of module to look for.
Returns:
- String of the module name.
+ A list of the module names.
"""
testable_modules = []
for mod in self.module_info.get_module_names(path):
@@ -72,7 +68,8 @@
# the test and another to run it. For now, we are assuming they are
# isolated in their own folders and will return if we find one.
if self.module_info.is_robolectric_test(mod):
- return mod
+ # return a list with one module name if it is robolectric.
+ return [mod]
if self.module_info.is_testable_module(mod_info):
testable_modules.append(mod_info.get(constants.MODULE_NAME))
return test_finder_utils.extract_test_from_tests(testable_modules)
@@ -111,12 +108,13 @@
out_dir = os.path.relpath(out_dir, self.root_dir)
vts_out_dir = os.path.join(out_dir, 'vts', 'android-vts', 'testcases')
# Parse dependency of default staging plans.
-
- xml_path = test_finder_utils.search_integration_dirs(
+ xml_paths = test_finder_utils.search_integration_dirs(
constants.VTS_STAGING_PLAN,
self.module_info.get_paths(constants.VTS_TF_MODULE))
- vts_xmls = test_finder_utils.get_plans_from_vts_xml(xml_path)
+ vts_xmls = set()
vts_xmls.add(config_file)
+ for xml_path in xml_paths:
+ vts_xmls |= test_finder_utils.get_plans_from_vts_xml(xml_path)
for config_file in vts_xmls:
# Add in vts test build targets.
test.build_targets |= test_finder_utils.get_targets_from_vts_xml(
@@ -234,13 +232,13 @@
test_finder_utils.get_cc_filter(
kwargs.get('class_name', '*'), methods), frozenset())])
# Path to java file.
- elif file_name and _JAVA_EXT_RE.match(file_name):
+ elif file_name and constants.JAVA_EXT_RE.match(file_name):
full_class_name = test_finder_utils.get_fully_qualified_class_name(
path)
ti_filter = frozenset(
[test_info.TestFilter(full_class_name, methods)])
# Path to cc file.
- elif file_name and _CC_EXT_RE.match(file_name):
+ elif file_name and constants.CC_EXT_RE.match(file_name):
if not test_finder_utils.has_cc_class(path):
raise atest_error.MissingCCTestCaseError(
"Can't find CC class in %s" % path)
@@ -254,7 +252,7 @@
os.path.relpath(path, self.root_dir)):
dir_items = [os.path.join(path, f) for f in os.listdir(path)]
for dir_item in dir_items:
- if _JAVA_EXT_RE.match(dir_item):
+ if constants.JAVA_EXT_RE.match(dir_item):
package_name = test_finder_utils.get_package_name(dir_item)
if package_name:
# methods should be empty frozenset for package.
@@ -283,7 +281,7 @@
return os.path.join(rel_module_dir, constants.MODULE_CONFIG)
return None
- def _get_test_info(self, test_path, rel_config, module_name, test_filter):
+ def _get_test_infos(self, test_path, rel_config, module_name, test_filter):
"""Get test_info for test_path.
Args:
@@ -293,24 +291,32 @@
test_filter: A test info filter.
Returns:
- TestInfo namedtuple if found, else None.
+ A list of TestInfo namedtuple if found, else None.
"""
if not rel_config:
rel_config = self._get_rel_config(test_path)
if not rel_config:
return None
- if not module_name:
- module_name = self._determine_testable_module(
+ if module_name:
+ module_names = [module_name]
+ else:
+ module_names = self._determine_testable_module(
os.path.dirname(rel_config))
- # The real test config might be recorded in module-info.
- rel_config = self._get_module_test_config(module_name,
- rel_config=rel_config)
- return self._process_test_info(test_info.TestInfo(
- test_name=module_name,
- test_runner=self._TEST_RUNNER,
- build_targets=set(),
- data={constants.TI_FILTER: test_filter,
- constants.TI_REL_CONFIG: rel_config}))
+ test_infos = []
+ if module_names:
+ for mname in module_names:
+ # The real test config might be record in module-info.
+ rel_config = self._get_module_test_config(mname,
+ rel_config=rel_config)
+ tinfo = self._process_test_info(test_info.TestInfo(
+ test_name=mname,
+ test_runner=self._TEST_RUNNER,
+ build_targets=set(),
+ data={constants.TI_FILTER: test_filter,
+ constants.TI_REL_CONFIG: rel_config}))
+ if tinfo:
+ test_infos.append(tinfo)
+ return test_infos
def find_test_by_module_name(self, module_name):
"""Find test for the given module name.
@@ -319,7 +325,8 @@
module_name: A string of the test's module name.
Returns:
- A populated TestInfo namedtuple if found, else None.
+ A list that includes only 1 populated TestInfo namedtuple
+ if found, otherwise None.
"""
mod_info = self.module_info.get_module_info(module_name)
if self.module_info.is_testable_module(mod_info):
@@ -327,12 +334,14 @@
rel_config = os.path.join(mod_info['path'][0],
constants.MODULE_CONFIG)
rel_config = self._get_module_test_config(module_name, rel_config=rel_config)
- return self._process_test_info(test_info.TestInfo(
+ tinfo = self._process_test_info(test_info.TestInfo(
test_name=module_name,
test_runner=self._TEST_RUNNER,
build_targets=set(),
data={constants.TI_REL_CONFIG: rel_config,
constants.TI_FILTER: frozenset()}))
+ if tinfo:
+ return [tinfo]
return None
def find_test_by_class_name(self, class_name, module_name=None,
@@ -350,7 +359,7 @@
native test or not.
Returns:
- A populated TestInfo namedtuple if test found, else None.
+ A list of populated TestInfo namedtuple if test found, else None.
"""
class_name, methods = test_finder_utils.split_methods(class_name)
if rel_config:
@@ -358,22 +367,27 @@
os.path.dirname(rel_config))
else:
search_dir = self.root_dir
- test_path = test_finder_utils.find_class_file(search_dir, class_name,
- is_native_test)
- if not test_path and rel_config:
+ test_paths = test_finder_utils.find_class_file(search_dir, class_name,
+ is_native_test, methods)
+ if not test_paths and rel_config:
logging.info('Did not find class (%s) under module path (%s), '
'researching from repo root.', class_name, rel_config)
- test_path = test_finder_utils.find_class_file(self.root_dir,
- class_name,
- is_native_test)
- if not test_path:
+ test_paths = test_finder_utils.find_class_file(self.root_dir,
+ class_name,
+ is_native_test,
+ methods)
+ if not test_paths:
return None
- test_filter = self._get_test_info_filter(
- test_path, methods, class_name=class_name,
- is_native_test=is_native_test)
- tinfo = self._get_test_info(test_path, rel_config, module_name,
- test_filter)
- return tinfo
+ tinfos = []
+ for test_path in test_paths:
+ test_filter = self._get_test_info_filter(
+ test_path, methods, class_name=class_name,
+ is_native_test=is_native_test)
+ tinfo = self._get_test_infos(test_path, rel_config,
+ module_name, test_filter)
+ if tinfo:
+ tinfos.extend(tinfo)
+ return tinfos
def find_test_by_module_and_class(self, module_class):
"""Find the test info given a MODULE:CLASS string.
@@ -382,12 +396,14 @@
module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD.
Returns:
- A populated TestInfo namedtuple if found, else None.
+ A list of populated TestInfo namedtuple if found, else None.
"""
if ':' not in module_class:
return None
module_name, class_name = module_class.split(':')
- module_info = self.find_test_by_module_name(module_name)
+ # module_infos is a list with at most 1 element.
+ module_infos = self.find_test_by_module_name(module_name)
+ module_info = module_infos[0] if module_infos else None
if not module_info:
return None
# If the target module is NATIVE_TEST, search CC classes only.
@@ -414,7 +430,7 @@
ref_config: Optional. A string of rel path of config.
Returns:
- A populated TestInfo namedtuple if found, else None.
+ A list of populated TestInfo namedtuple if found, else None.
"""
_, methods = test_finder_utils.split_methods(package)
if methods:
@@ -427,16 +443,20 @@
os.path.dirname(rel_config))
else:
search_dir = self.root_dir
- package_path = test_finder_utils.run_find_cmd(
+ package_paths = test_finder_utils.run_find_cmd(
test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir,
package.replace('.', '/'))
# Package path will be the full path to the dir represented by package.
- if not package_path:
+ if not package_paths:
return None
test_filter = frozenset([test_info.TestFilter(package, frozenset())])
- tinfo = self._get_test_info(package_path, rel_config, module_name,
- test_filter)
- return tinfo
+ test_infos = []
+ for package_path in package_paths:
+ tinfo = self._get_test_infos(package_path, rel_config,
+ module_name, test_filter)
+ if tinfo:
+ test_infos.extend(tinfo)
+ return test_infos
def find_test_by_module_and_package(self, module_package):
"""Find the test info given a MODULE:PACKAGE string.
@@ -445,10 +465,12 @@
module_package: A string of form MODULE:PACKAGE
Returns:
- A populated TestInfo namedtuple if found, else None.
+ A list of populated TestInfo namedtuple if found, else None.
"""
module_name, package = module_package.split(':')
- module_info = self.find_test_by_module_name(module_name)
+ # module_infos is a list with at most 1 element.
+ module_infos = self.find_test_by_module_name(module_name)
+ module_info = module_infos[0] if module_infos else None
if not module_info:
return None
return self.find_test_by_package_name(
@@ -470,7 +492,7 @@
path: A string of the test's path.
Returns:
- A populated TestInfo namedtuple if test found, else None
+ A list of populated TestInfo namedtuple if test found, else None
"""
logging.debug('Finding test by path: %s', path)
path, methods = test_finder_utils.split_methods(path)
@@ -479,6 +501,9 @@
path = os.path.realpath(path)
if not os.path.exists(path):
return None
+ if (methods and
+ not test_finder_utils.has_method_in_file(path, methods)):
+ return None
dir_path, _ = test_finder_utils.get_dir_path_and_filename(path)
# Module/Class
rel_module_dir = test_finder_utils.find_parent_module_dir(
@@ -488,7 +513,7 @@
rel_config = os.path.join(rel_module_dir, constants.MODULE_CONFIG)
test_filter = self._get_test_info_filter(path, methods,
rel_module_dir=rel_module_dir)
- return self._get_test_info(path, rel_config, None, test_filter)
+ return self._get_test_infos(path, rel_config, None, test_filter)
def find_test_by_cc_class_name(self, class_name, module_name=None,
rel_config=None):
@@ -503,7 +528,7 @@
rel_config: Optional. A string of module dir relative to repo root.
Returns:
- A populated TestInfo namedtuple if test found, else None.
+ A list of populated TestInfo namedtuple if test found, else None.
"""
# Check if class_name is prepended with file name. If so, trim the
# prefix and keep only the class_name.
diff --git a/atest/test_finders/module_finder_unittest.py b/atest/test_finders/module_finder_unittest.py
index 2d6124f..14041b0 100755
--- a/atest/test_finders/module_finder_unittest.py
+++ b/atest/test_finders/module_finder_unittest.py
@@ -110,14 +110,17 @@
'path': [uc.MODULE_DIR],
constants.MODULE_CLASS: []}
self.mod_finder.module_info.get_module_info.return_value = mod_info
+ t_infos = self.mod_finder.find_test_by_module_name(uc.MODULE_NAME)
unittest_utils.assert_equal_testinfos(
self,
- self.mod_finder.find_test_by_module_name(uc.MODULE_NAME),
+ t_infos[0],
uc.MODULE_INFO)
self.mod_finder.module_info.get_module_info.return_value = None
self.mod_finder.module_info.is_testable_module.return_value = False
self.assertIsNone(self.mod_finder.find_test_by_module_name('Not_Module'))
+ @mock.patch.object(test_finder_utils, 'has_method_in_file',
+ return_value=True)
@mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
return_value=False)
@mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -129,7 +132,7 @@
#pylint: disable=unused-argument
def test_find_test_by_class_name(self, _isdir, _isfile, _fqcn,
mock_checkoutput, mock_build,
- _vts):
+ _vts, _has_method_in_file):
"""Test find_test_by_class_name."""
mock_build.return_value = uc.CLASS_BUILD_TARGETS
self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
@@ -140,37 +143,42 @@
constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
constants.MODULE_NAME: uc.MODULE_NAME,
constants.MODULE_CLASS: []}
+ t_infos = self.mod_finder.find_test_by_class_name(uc.CLASS_NAME)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_class_name(uc.CLASS_NAME), uc.CLASS_INFO)
+ self, t_infos[0], uc.CLASS_INFO)
# with method
mock_build.return_value = uc.MODULE_BUILD_TARGETS
class_with_method = '%s#%s' % (uc.CLASS_NAME, uc.METHOD_NAME)
+ t_infos = self.mod_finder.find_test_by_class_name(class_with_method)
unittest_utils.assert_equal_testinfos(
- self,
- self.mod_finder.find_test_by_class_name(class_with_method),
- uc.METHOD_INFO)
+ self, t_infos[0], uc.METHOD_INFO)
mock_build.return_value = uc.MODULE_BUILD_TARGETS
class_methods = '%s,%s' % (class_with_method, uc.METHOD2_NAME)
+ t_infos = self.mod_finder.find_test_by_class_name(class_methods)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_class_name(class_methods),
+ self, t_infos[0],
FLAT_METHOD_INFO)
# module and rel_config passed in
mock_build.return_value = uc.CLASS_BUILD_TARGETS
+ t_infos = self.mod_finder.find_test_by_class_name(
+ uc.CLASS_NAME, uc.MODULE_NAME, uc.CONFIG_FILE)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_class_name(
- uc.CLASS_NAME, uc.MODULE_NAME, uc.CONFIG_FILE), uc.CLASS_INFO)
+ self, t_infos[0], uc.CLASS_INFO)
# find output fails to find class file
mock_checkoutput.return_value = ''
self.assertIsNone(self.mod_finder.find_test_by_class_name('Not class'))
# class is outside given module path
mock_checkoutput.side_effect = classoutside_side_effect
- unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_class_name(uc.CLASS_NAME,
+ t_infos = self.mod_finder.find_test_by_class_name(uc.CLASS_NAME,
uc.MODULE2_NAME,
- uc.CONFIG2_FILE),
+ uc.CONFIG2_FILE)
+ unittest_utils.assert_equal_testinfos(
+ self, t_infos[0],
CLASS_INFO_MODULE_2)
+ @mock.patch.object(test_finder_utils, 'has_method_in_file',
+ return_value=True)
@mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
return_value=False)
@mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -181,7 +189,7 @@
#pylint: disable=unused-argument
def test_find_test_by_module_and_class(self, _isfile, _fqcn,
mock_checkoutput, mock_build,
- _vts):
+ _vts, _has_method_in_file):
"""Test find_test_by_module_and_class."""
# Native test was tested in test_find_test_by_cc_class_name().
self.mod_finder.module_info.is_native_test.return_value = False
@@ -193,12 +201,12 @@
constants.MODULE_PATH: [uc.MODULE_DIR],
constants.MODULE_CLASS: []}
self.mod_finder.module_info.get_module_info.return_value = mod_info
- t_info = self.mod_finder.find_test_by_module_and_class(MODULE_CLASS)
- unittest_utils.assert_equal_testinfos(self, t_info, uc.CLASS_INFO)
+ t_infos = self.mod_finder.find_test_by_module_and_class(MODULE_CLASS)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.CLASS_INFO)
# with method
mock_build.return_value = uc.MODULE_BUILD_TARGETS
- t_info = self.mod_finder.find_test_by_module_and_class(MODULE_CLASS_METHOD)
- unittest_utils.assert_equal_testinfos(self, t_info, uc.METHOD_INFO)
+ t_infos = self.mod_finder.find_test_by_module_and_class(MODULE_CLASS_METHOD)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.METHOD_INFO)
self.mod_finder.module_info.is_testable_module.return_value = False
# bad module, good class, returns None
bad_module = '%s:%s' % ('BadMod', uc.CLASS_NAME)
@@ -232,13 +240,13 @@
constants.MODULE_PATH: [uc.CC_MODULE_DIR],
constants.MODULE_CLASS: []}
self.mod_finder.module_info.get_module_info.return_value = mod_info
- t_info = self.mod_finder.find_test_by_module_and_class(CC_MODULE_CLASS)
- unittest_utils.assert_equal_testinfos(self, t_info, uc.CC_MODULE_CLASS_INFO)
+ t_infos = self.mod_finder.find_test_by_module_and_class(CC_MODULE_CLASS)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.CC_MODULE_CLASS_INFO)
# with method
mock_build.return_value = uc.MODULE_BUILD_TARGETS
mock_fcf.side_effect = [None, None, '/']
- t_info = self.mod_finder.find_test_by_module_and_class(CC_MODULE_CLASS_METHOD)
- unittest_utils.assert_equal_testinfos(self, t_info, uc.CC_METHOD_INFO)
+ t_infos = self.mod_finder.find_test_by_module_and_class(CC_MODULE_CLASS_METHOD)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.CC_METHOD_INFO)
# bad module, good class, returns None
bad_module = '%s:%s' % ('BadMod', uc.CC_CLASS_NAME)
self.mod_finder.module_info.get_module_info.return_value = None
@@ -264,8 +272,9 @@
constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
constants.MODULE_NAME: uc.MODULE_NAME,
constants.MODULE_CLASS: []}
+ t_infos = self.mod_finder.find_test_by_package_name(uc.PACKAGE)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_package_name(uc.PACKAGE),
+ self, t_infos[0],
uc.PACKAGE_INFO)
# with method, should raise
pkg_with_method = '%s#%s' % (uc.PACKAGE, uc.METHOD_NAME)
@@ -273,9 +282,10 @@
self.mod_finder.find_test_by_package_name,
pkg_with_method)
# module and rel_config passed in
+ t_infos = self.mod_finder.find_test_by_package_name(
+ uc.PACKAGE, uc.MODULE_NAME, uc.CONFIG_FILE)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_package_name(
- uc.PACKAGE, uc.MODULE_NAME, uc.CONFIG_FILE), uc.PACKAGE_INFO)
+ self, t_infos[0], uc.PACKAGE_INFO)
# find output fails to find class file
mock_checkoutput.return_value = ''
self.assertIsNone(self.mod_finder.find_test_by_package_name('Not pkg'))
@@ -297,8 +307,8 @@
constants.MODULE_PATH: [uc.MODULE_DIR],
constants.MODULE_CLASS: []}
self.mod_finder.module_info.get_module_info.return_value = mod_info
- t_info = self.mod_finder.find_test_by_module_and_package(MODULE_PACKAGE)
- unittest_utils.assert_equal_testinfos(self, t_info, uc.PACKAGE_INFO)
+ t_infos = self.mod_finder.find_test_by_module_and_package(MODULE_PACKAGE)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.PACKAGE_INFO)
# with method, raises
module_pkg_with_method = '%s:%s#%s' % (uc.MODULE2_NAME, uc.PACKAGE,
uc.METHOD_NAME)
@@ -316,6 +326,8 @@
self.mod_finder.module_info.get_module_info.return_value = mod_info
self.assertIsNone(self.mod_finder.find_test_by_module_and_package(bad_pkg))
+ @mock.patch.object(test_finder_utils, 'has_method_in_file',
+ return_value=True)
@mock.patch.object(test_finder_utils, 'has_cc_class',
return_value=True)
@mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -330,7 +342,8 @@
@mock.patch('os.path.exists')
#pylint: disable=unused-argument
def test_find_test_by_path(self, mock_pathexists, mock_dir, _isfile, _real,
- _fqcn, _vts, mock_build, _has_cc_class):
+ _fqcn, _vts, mock_build, _has_cc_class,
+ _has_method_in_file):
"""Test find_test_by_path."""
self.mod_finder.module_info.is_robolectric_test.return_value = False
self.mod_finder.module_info.has_test_config.return_value = True
@@ -354,23 +367,27 @@
class_path = '%s.kt' % uc.CLASS_NAME
mock_build.return_value = uc.CLASS_BUILD_TARGETS
+ t_infos = self.mod_finder.find_test_by_path(class_path)
unittest_utils.assert_equal_testinfos(
- self, uc.CLASS_INFO, self.mod_finder.find_test_by_path(class_path))
+ self, uc.CLASS_INFO, t_infos[0])
class_path = '%s.java' % uc.CLASS_NAME
mock_build.return_value = uc.CLASS_BUILD_TARGETS
+ t_infos = self.mod_finder.find_test_by_path(class_path)
unittest_utils.assert_equal_testinfos(
- self, uc.CLASS_INFO, self.mod_finder.find_test_by_path(class_path))
+ self, uc.CLASS_INFO, t_infos[0])
class_with_method = '%s#%s' % (class_path, uc.METHOD_NAME)
mock_build.return_value = uc.MODULE_BUILD_TARGETS
+ t_infos = self.mod_finder.find_test_by_path(class_with_method)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_path(class_with_method), uc.METHOD_INFO)
+ self, t_infos[0], uc.METHOD_INFO)
class_with_methods = '%s,%s' % (class_with_method, uc.METHOD2_NAME)
mock_build.return_value = uc.MODULE_BUILD_TARGETS
+ t_infos = self.mod_finder.find_test_by_path(class_with_methods)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_path(class_with_methods),
+ self, t_infos[0],
FLAT_METHOD_INFO)
# Cc path testing.
@@ -382,8 +399,9 @@
mock_dir.return_value = uc.CC_MODULE_DIR
class_path = '%s' % uc.CC_PATH
mock_build.return_value = uc.CLASS_BUILD_TARGETS
+ t_infos = self.mod_finder.find_test_by_path(class_path)
unittest_utils.assert_equal_testinfos(
- self, uc.CC_PATH_INFO2, self.mod_finder.find_test_by_path(class_path))
+ self, uc.CC_PATH_INFO2, t_infos[0])
@mock.patch.object(module_finder.ModuleFinder, '_get_build_targets',
return_value=uc.MODULE_BUILD_TARGETS)
@@ -404,13 +422,15 @@
constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
constants.MODULE_NAME: uc.MODULE_NAME,
constants.MODULE_CLASS: []}
+ t_infos = self.mod_finder.find_test_by_path(class_dir)
unittest_utils.assert_equal_testinfos(
- self, uc.PATH_INFO, self.mod_finder.find_test_by_path(class_dir))
+ self, uc.PATH_INFO, t_infos[0])
# Dir with no java files in it, should run whole module
empty_dir = os.path.join(uc.TEST_DATA_DIR, 'path_testing_empty')
+ t_infos = self.mod_finder.find_test_by_path(empty_dir)
unittest_utils.assert_equal_testinfos(
self, uc.EMPTY_PATH_INFO,
- self.mod_finder.find_test_by_path(empty_dir))
+ t_infos[0])
# Dir with cc files in it, should run as cc class
class_dir = os.path.join(uc.TEST_DATA_DIR, 'cc_path_testing')
self.mod_finder.module_info.get_module_names.return_value = [uc.CC_MODULE_NAME]
@@ -418,9 +438,12 @@
constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
constants.MODULE_NAME: uc.CC_MODULE_NAME,
constants.MODULE_CLASS: []}
+ t_infos = self.mod_finder.find_test_by_path(class_dir)
unittest_utils.assert_equal_testinfos(
- self, uc.CC_PATH_INFO, self.mod_finder.find_test_by_path(class_dir))
+ self, uc.CC_PATH_INFO, t_infos[0])
+ @mock.patch.object(test_finder_utils, 'has_method_in_file',
+ return_value=True)
@mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
return_value=False)
@mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
@@ -430,7 +453,7 @@
#pylint: disable=unused-argument
def test_find_test_by_cc_class_name(self, _isdir, _isfile,
mock_checkoutput, mock_build,
- _vts):
+ _vts, _has_method):
"""Test find_test_by_cc_class_name."""
mock_build.return_value = uc.CLASS_BUILD_TARGETS
self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
@@ -441,36 +464,42 @@
constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
constants.MODULE_NAME: uc.CC_MODULE_NAME,
constants.MODULE_CLASS: []}
+ t_infos = self.mod_finder.find_test_by_cc_class_name(uc.CC_CLASS_NAME)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_cc_class_name(uc.CC_CLASS_NAME), uc.CC_CLASS_INFO)
+ self, t_infos[0], uc.CC_CLASS_INFO)
# with method
mock_build.return_value = uc.MODULE_BUILD_TARGETS
class_with_method = '%s#%s' % (uc.CC_CLASS_NAME, uc.CC_METHOD_NAME)
+ t_infos = self.mod_finder.find_test_by_cc_class_name(class_with_method)
unittest_utils.assert_equal_testinfos(
self,
- self.mod_finder.find_test_by_cc_class_name(class_with_method),
+ t_infos[0],
uc.CC_METHOD_INFO)
mock_build.return_value = uc.MODULE_BUILD_TARGETS
class_methods = '%s,%s' % (class_with_method, uc.CC_METHOD2_NAME)
+ t_infos = self.mod_finder.find_test_by_cc_class_name(class_methods)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_cc_class_name(class_methods),
+ self, t_infos[0],
uc.CC_METHOD2_INFO)
# module and rel_config passed in
mock_build.return_value = uc.CLASS_BUILD_TARGETS
+ t_infos = self.mod_finder.find_test_by_cc_class_name(
+ uc.CC_CLASS_NAME, uc.CC_MODULE_NAME, uc.CC_CONFIG_FILE)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_cc_class_name(
- uc.CC_CLASS_NAME, uc.CC_MODULE_NAME, uc.CC_CONFIG_FILE), uc.CC_CLASS_INFO)
+ self, t_infos[0], uc.CC_CLASS_INFO)
# find output fails to find class file
mock_checkoutput.return_value = ''
self.assertIsNone(self.mod_finder.find_test_by_cc_class_name(
'Not class'))
# class is outside given module path
mock_checkoutput.return_value = uc.CC_FIND_ONE
+ t_infos = self.mod_finder.find_test_by_cc_class_name(
+ uc.CC_CLASS_NAME,
+ uc.CC_MODULE2_NAME,
+ uc.CC_CONFIG2_FILE)
unittest_utils.assert_equal_testinfos(
- self, self.mod_finder.find_test_by_cc_class_name(uc.CC_CLASS_NAME,
- uc.CC_MODULE2_NAME,
- uc.CC_CONFIG2_FILE),
+ self, t_infos[0],
CC_CLASS_INFO_MODULE_2)
def test_get_testable_modules_with_ld(self):
diff --git a/atest/test_finders/suite_plan_finder.py b/atest/test_finders/suite_plan_finder.py
index 1fdccd2..a33da2d 100644
--- a/atest/test_finders/suite_plan_finder.py
+++ b/atest/test_finders/suite_plan_finder.py
@@ -105,7 +105,8 @@
suite_path: A string of the path to the test's file or dir.
Returns:
- A populated TestInfo namedtuple if test found, else None.
+ A list of populated TestInfo namedtuple if test found, else None.
+ This is a list with at most 1 element.
"""
path, _ = test_finder_utils.split_methods(suite_path)
# Make sure we're looking for a config.
@@ -116,7 +117,7 @@
path, self.suite_plan_dirs)
if suite_plan_dir:
rel_config = os.path.relpath(path, self.root_dir)
- return self._get_test_info_from_path(rel_config)
+ return [self._get_test_info_from_path(rel_config)]
return None
def find_test_by_suite_name(self, suite_name):
@@ -133,19 +134,25 @@
suite_name: A string of suite name.
Returns:
- A populated TestInfo namedtuple if suite_name matches
+ A list of populated TestInfo namedtuple if suite_name matches
a suite in constants.SUITE_PLAN, else check if the file
existing in the suite plan dirs, else return None.
"""
logging.debug('Finding test by suite: %s', suite_name)
+ test_infos = []
if suite_name in constants.SUITE_PLANS:
- return test_info.TestInfo(
+ test_infos.append(test_info.TestInfo(
test_name=suite_name,
test_runner=self._SUITE_PLAN_TEST_RUNNER,
build_targets=set([suite_name]),
- suite=suite_name)
- test_file = test_finder_utils.search_integration_dirs(
- suite_name, self.suite_plan_dirs)
- if test_file is None:
- return None
- return self._get_test_info_from_path(test_file, suite_name)
+ suite=suite_name))
+ else:
+ test_files = test_finder_utils.search_integration_dirs(
+ suite_name, self.suite_plan_dirs)
+ if not test_files:
+ return None
+ for test_file in test_files:
+ _test_info = self._get_test_info_from_path(test_file, suite_name)
+ if _test_info:
+ test_infos.append(_test_info)
+ return test_infos
diff --git a/atest/test_finders/suite_plan_finder_unittest.py b/atest/test_finders/suite_plan_finder_unittest.py
index 4e9eaa5..0fed2d2 100755
--- a/atest/test_finders/suite_plan_finder_unittest.py
+++ b/atest/test_finders/suite_plan_finder_unittest.py
@@ -103,7 +103,7 @@
test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
build_targets={suite_name},
suite=suite_name)
- unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+ unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
suite_name = 'CTS'
_search.return_value = None
@@ -113,13 +113,13 @@
suite_name = 'cts-common'
suite = 'cts'
- _search.return_value = os.path.join(uc.ROOT, uc.CTS_INT_DIR, suite_name + '.xml')
+ _search.return_value = [os.path.join(uc.ROOT, uc.CTS_INT_DIR, suite_name + '.xml')]
t_info = self.suite_plan_finder.find_test_by_suite_name(suite_name)
want_info = test_info.TestInfo(test_name=suite_name,
test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
build_targets=set([suite]),
suite=suite)
- unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+ unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
@mock.patch('os.path.realpath',
side_effect=unittest_utils.realpath_side_effect)
@@ -155,7 +155,7 @@
test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
build_targets=set([suite]),
suite=suite)
- unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+ unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
suite_int_name = 'cts-common'
suite = 'cts'
@@ -166,7 +166,7 @@
test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
build_targets=set([suite]),
suite=suite)
- unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+ unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
suite_int_name = 'cts-camera'
suite = 'cts'
@@ -177,7 +177,7 @@
test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
build_targets=set([suite]),
suite=suite)
- unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+ unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
if __name__ == '__main__':
diff --git a/atest/test_finders/test_finder_utils.py b/atest/test_finders/test_finder_utils.py
index 0fc84b9..63d9014 100644
--- a/atest/test_finders/test_finder_utils.py
+++ b/atest/test_finders/test_finder_utils.py
@@ -35,8 +35,17 @@
# We want to make sure we don't grab apks with paths in their name since we
# assume the apk name is the build target.
_APK_RE = re.compile(r'^[^/]+\.apk$', re.I)
-# RE for check if TEST or TEST_F is in a cc file or not.
-_CC_CLASS_RE = re.compile(r'TEST(_F)?\(', re.I)
+# RE for checking if TEST or TEST_F is in a cc file or not.
+_CC_CLASS_RE = re.compile(r'TEST(_F)?[ ]*\(', re.I)
+# RE for checking if there exists one of the methods in java file.
+_JAVA_METHODS_PATTERN = r'.*[ ]+({0})\(.*'
+# RE for checking if there exists one of the methods in cc file.
+_CC_METHODS_PATTERN = r'[ ]*TEST(_F|_P)?[ ]*\(.*,[ ]*({0})\).*'
+# RE for checking if finding output matches the cc format.
+# e.g. file_path:TEST_F(test_name, method_name){
+_CC_OUTPUT_RE = re.compile(r'(?P<file_path>/.*):[ ]*TEST(_F|_P)?[ ]*\('
+ r'(?P<test_name>\w+)[ ]*,[ ]*(?P<method_name>\w+)\)'
+ r'[ ]*\{')
# Parse package name from the package declaration line of a java or a kotlin file.
# Group matches "foo.bar" of line "package foo.bar;" or "package foo.bar"
_PACKAGE_RE = re.compile(r'\s*package\s+(?P<package>[^(;|\s)]+)\s*', re.I)
@@ -46,10 +55,10 @@
# Explanation of FIND_REFERENCE_TYPEs:
# ----------------------------------
-# 0. CLASS: Name of a java/kotlin class, usually file is named the same (HostTest lives
-# in HostTest.java or HostTest.kt)
+# 0. CLASS: Name of a java/kotlin class, usually file is named the same
+# (HostTest lives in HostTest.java or HostTest.kt)
# 1. QUALIFIED_CLASS: Like CLASS but also contains the package in front like
-#. com.android.tradefed.testtype.HostTest.
+# com.android.tradefed.testtype.HostTest.
# 2. PACKAGE: Name of a java package.
# 3. INTEGRATION: XML file name in one of the 4 integration config directories.
# 4. CC_CLASS: Name of a cc class.
@@ -57,27 +66,27 @@
FIND_REFERENCE_TYPE = atest_enum.AtestEnum(['CLASS', 'QUALIFIED_CLASS',
'PACKAGE', 'INTEGRATION', 'CC_CLASS'])
# Get cpu count.
-_CPU_COUNT = 1
-try:
- _CPU_COUNT = multiprocessing.cpu_count()
-except NotImplementedError:
- pass
+_CPU_COUNT = 0 if os.uname()[0] == 'Linux' else multiprocessing.cpu_count()
+
# Unix find commands for searching for test files based on test type input.
# Note: Find (unlike grep) exits with status 0 if nothing found.
FIND_CMDS = {
- FIND_REFERENCE_TYPE.CLASS: r"find {0} -type d {1} -prune -o -type f "
- r"\( -name '*{2}.java' -o -name '*{2}.kt' \) -print",
- FIND_REFERENCE_TYPE.QUALIFIED_CLASS: r"find {0} -type d {1} -prune -o "
- r"\( -wholename '*{2}.java' "
- r"-o -wholename '*{2}.kt' \) -print",
- FIND_REFERENCE_TYPE.PACKAGE: r"find {0} -type d {1} -prune -o -wholename "
+ FIND_REFERENCE_TYPE.CLASS: r"find {0} {1} -type f"
+ r"| egrep '.*/{2}\.(kt|java)$' || true",
+ FIND_REFERENCE_TYPE.QUALIFIED_CLASS: r"find {0} {1} -type f"
+ r"| egrep '.*{2}\.(kt|java)$' || true",
+ FIND_REFERENCE_TYPE.PACKAGE: r"find {0} {1} -wholename "
r"'*{2}' -type d -print",
- FIND_REFERENCE_TYPE.INTEGRATION: r"find {0} -type d {1} -prune -o -wholename "
+ FIND_REFERENCE_TYPE.INTEGRATION: r"find {0} {1} -wholename "
r"'*{2}.xml' -print",
- FIND_REFERENCE_TYPE.CC_CLASS: r"find {0} -type d {1} -prune -o -type f "
- r"\( -name '*.cpp' -o -name '*.cc' \)"
- r" | xargs -P " + str(_CPU_COUNT) +
- r" grep -s -H -E 'TEST(_F)?\({2},' {{}} + || true"
+ # Searching a test among files where the absolute paths contain *test*.
+ # If users complain atest couldn't find a CC_CLASS, ask them to follow the
+ # convention that the filename or dirname must contain *test*, where *test*
+ # is case-insensitive.
+ FIND_REFERENCE_TYPE.CC_CLASS: r"find {0} {1} -type f -print"
+ r"| egrep -i '/*test.*\.(cc|cpp)$'"
+ r"| xargs -P" + str(_CPU_COUNT) +
+ r" egrep -sH '[ ]*TEST(_F|_P)?[ ]*\({2}' || true"
}
# XML parsing related constants.
@@ -206,7 +215,38 @@
return match.group('package')
-def extract_test_path(output, is_native_test=False):
+def has_method_in_file(test_path, methods):
+ """Find out if there is at least one method in the file.
+
+ Note: This method doesn't handle if method is in comment sections or not.
+ If the file has any method(even in comment sections), it will return True.
+
+ Args:
+ test_path: A string of absolute path to the test file.
+ methods: A set of method names.
+
+ Returns:
+ Boolean: there is at least one method in test_path.
+ """
+ if not os.path.isfile(test_path):
+ return False
+ methods_re = None
+ if constants.JAVA_EXT_RE.match(test_path):
+ methods_re = re.compile(_JAVA_METHODS_PATTERN.format(
+ '|'.join([r'%s' % x for x in methods])))
+ elif constants.CC_EXT_RE.match(test_path):
+ methods_re = re.compile(_CC_METHODS_PATTERN.format(
+ '|'.join([r'%s' % x for x in methods])))
+ if methods_re:
+ with open(test_path) as test_file:
+ for line in test_file:
+ match = re.match(methods_re, line)
+ if match:
+ return True
+ return False
+
+
+def extract_test_path(output, methods=None):
"""Extract the test path from the output of a unix 'find' command.
Example of find output for CLASS find cmd:
@@ -214,49 +254,74 @@
Args:
output: A string output of a unix 'find' command.
- is_native_test: A boolean variable of whether to search for a native
- test or not.
+ methods: A set of method names.
Returns:
- A string of the test path or None if output is '' or None.
+ A list of the test paths or None if output is '' or None.
"""
if not output:
return None
- tests = output.strip('\n').split('\n')
- if is_native_test:
- tests = list(set([path.split(":")[0] for path in tests]))
- return extract_test_from_tests(tests)
+ verified_tests = set()
+ output_lines = output.strip('\n').split('\n')
+ for test in output_lines:
+ # compare _CC_OUTPUT_RE with output
+ match_obj = _CC_OUTPUT_RE.match(test)
+ if match_obj:
+ # cc/cpp
+ fpath = match_obj.group('file_path')
+ if not methods or match_obj.group('method_name') in methods:
+ verified_tests.add(fpath)
+ else:
+ # java/kt
+ if not methods or has_method_in_file(test, methods):
+ verified_tests.add(test)
+ return extract_test_from_tests(list(verified_tests))
def extract_test_from_tests(tests):
"""Extract the test path from the tests.
Return the test to run from tests. If more than one option, prompt the user
- to select one.
+ to select multiple ones. Supporting formats:
+ - An integer. E.g. 0
+ - Comma-separated integers. E.g. 1,3,5
+ - A range of integers denoted by the starting integer separated from
+ the end integer by a dash, '-'. E.g. 1-3
Args:
tests: A string list which contains multiple test paths.
Returns:
- A string of the test path or None if tests is out-of-index or ''.
+ A string list of paths.
"""
count = len(tests)
- test_index = 0
- if count == 0:
- return None
- elif count > 1:
+ if count <= 1:
+ return tests if count else None
+ mtests = set()
+ try:
numbered_list = ['%s: %s' % (i, t) for i, t in enumerate(tests)]
+ numbered_list.append('%s: All' % count)
print('Multiple tests found:\n{0}'.format('\n'.join(numbered_list)))
- try:
- test_index = int(raw_input('Please enter number of test to use '
- 'or hit return to keep searching: '))
- if test_index not in range(count):
- logging.warn('The input %s is out-of-range(%s).',
- test_index, (count-1))
- return None
- except ValueError:
- return None
- return tests[test_index]
+ test_indices = raw_input("Please enter numbers of test to use. "
+ "If none of above option matched, keep "
+ "searching for other possible tests."
+ "\n(multiple selection is supported,"
+ " e.g. '1' or '0,1' or '0-2'): ")
+ for idx in re.sub(r'(\s)', '', test_indices).split(','):
+ indices = idx.split('-')
+ len_indices = len(indices)
+ if len_indices > 0:
+ start_index = min(int(indices[0]), int(indices[len_indices-1]))
+ end_index = max(int(indices[0]), int(indices[len_indices-1]))
+ # One of input is 'All', return all options.
+ if start_index == count or end_index == count:
+ return tests
+ mtests.update(tests[start_index:(end_index+1)])
+ except (ValueError, IndexError, AttributeError, TypeError) as err:
+ logging.debug('%s', err)
+ print('None of above option matched, keep searching for other'
+ ' possible tests...')
+ return list(mtests)
def static_var(varname, value):
@@ -321,23 +386,24 @@
A string of the prune condition of the ignore dirs.
"""
out_dirs = _get_ignored_dirs()
- prune_cond = r'\( -name ".*"'
+ prune_cond = r'-type d \( -name ".*"'
for out_dir in out_dirs:
prune_cond += r' -o -path %s' % out_dir
- prune_cond += r' \)'
+ prune_cond += r' \) -prune -o'
return prune_cond
-def run_find_cmd(ref_type, search_dir, target):
+def run_find_cmd(ref_type, search_dir, target, methods=None):
"""Find a path to a target given a search dir and a target name.
Args:
ref_type: An AtestEnum of the reference type.
search_dir: A string of the dirpath to search in.
target: A string of what you're trying to find.
+ methods: A set of method names.
Return:
- A string of the path to the target.
+ A list of the path to the target.
"""
prune_cond = _get_prune_cond_of_ignored_dirs()
find_cmd = FIND_CMDS[ref_type].format(search_dir, prune_cond, target)
@@ -347,12 +413,10 @@
out = subprocess.check_output(find_cmd, shell=True)
logging.debug('%s find completed in %ss', ref_name, time.time() - start)
logging.debug('%s find cmd out: %s', ref_name, out)
- if ref_type == FIND_REFERENCE_TYPE.CC_CLASS:
- return extract_test_path(out, True)
- return extract_test_path(out)
+ return extract_test_path(out, methods)
-def find_class_file(search_dir, class_name, is_native_test=False):
+def find_class_file(search_dir, class_name, is_native_test=False, methods=None):
"""Find a path to a class file given a search dir and a class name.
Args:
@@ -360,21 +424,22 @@
class_name: A string of the class to search for.
is_native_test: A boolean variable of whether to search for a native
test or not.
+ methods: A set of method names.
Return:
- A string of the path to the java/cc file.
+ A list of the path to the java/cc file.
"""
if is_native_test:
find_target = class_name
ref_type = FIND_REFERENCE_TYPE.CC_CLASS
- return run_find_cmd(ref_type, search_dir, find_target)
+ return run_find_cmd(ref_type, search_dir, find_target, methods)
if '.' in class_name:
find_target = class_name.replace('.', '/')
ref_type = FIND_REFERENCE_TYPE.QUALIFIED_CLASS
else:
find_target = class_name
ref_type = FIND_REFERENCE_TYPE.CLASS
- return run_find_cmd(ref_type, search_dir, find_target)
+ return run_find_cmd(ref_type, search_dir, find_target, methods)
def is_equal_or_sub_dir(sub_dir, parent_dir):
@@ -762,7 +827,7 @@
int_dirs: A list of path needed to be searched.
Returns:
- A string of the test path.
+ A list of the test path.
Ask user to select if multiple tests are found.
None if no matched test found.
"""
@@ -770,10 +835,10 @@
test_files = []
for integration_dir in int_dirs:
abs_path = os.path.join(root_dir, integration_dir)
- test_file = run_find_cmd(FIND_REFERENCE_TYPE.INTEGRATION, abs_path,
- name)
- if test_file:
- test_files.append(test_file)
+ test_paths = run_find_cmd(FIND_REFERENCE_TYPE.INTEGRATION, abs_path,
+ name)
+ if test_paths:
+ test_files.extend(test_paths)
return extract_test_from_tests(test_files)
diff --git a/atest/test_finders/test_finder_utils_unittest.py b/atest/test_finders/test_finder_utils_unittest.py
index 2e77ed6..4c17678 100755
--- a/atest/test_finders/test_finder_utils_unittest.py
+++ b/atest/test_finders/test_finder_utils_unittest.py
@@ -31,10 +31,13 @@
CLASS_DIR = 'foo/bar/jank/src/android/jank/cts/ui'
OTHER_DIR = 'other/dir/'
OTHER_CLASS_NAME = 'test.java'
+CLASS_NAME3 = 'test2'
INT_DIR1 = os.path.join(uc.TEST_DATA_DIR, 'integration_dir_testing/int_dir1')
INT_DIR2 = os.path.join(uc.TEST_DATA_DIR, 'integration_dir_testing/int_dir2')
INT_FILE_NAME = 'int_dir_testing'
FIND_TWO = uc.ROOT + 'other/dir/test.java\n' + uc.FIND_ONE
+FIND_THREE = '/a/b/c.java\n/d/e/f.java\n/g/h/i.java'
+FIND_THREE_LIST = ['/a/b/c.java', '/d/e/f.java', '/g/h/i.java']
VTS_XML = 'VtsAndroidTest.xml'
VTS_BITNESS_XML = 'VtsBitnessAndroidTest.xml'
VTS_PUSH_DIR = 'vts_push_files'
@@ -114,33 +117,97 @@
test_finder_utils.split_methods('foo/bar/class.java#Method'),
('foo/bar/class.java', {'Method'}))
+ @mock.patch.object(test_finder_utils, 'has_method_in_file',
+ return_value=False)
@mock.patch('__builtin__.raw_input', return_value='1')
- def test_extract_test_path(self, _):
+ def test_extract_test_path(self, _, has_method):
"""Test extract_test_dir method."""
- path = os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')
+ paths = [os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')]
unittest_utils.assert_strict_equal(
- self, test_finder_utils.extract_test_path(uc.FIND_ONE), path)
- path = os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')
+ self, test_finder_utils.extract_test_path(uc.FIND_ONE), paths)
+ paths = [os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')]
unittest_utils.assert_strict_equal(
- self, test_finder_utils.extract_test_path(FIND_TWO), path)
+ self, test_finder_utils.extract_test_path(FIND_TWO), paths)
+ paths = None
+ unittest_utils.assert_strict_equal(
+ self, test_finder_utils.extract_test_path(uc.FIND_ONE, 'method'), paths)
+ has_method.return_value = True
+ paths = [os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')]
+ unittest_utils.assert_strict_equal(
+ self, test_finder_utils.extract_test_path(uc.FIND_ONE, 'method'), paths)
+
+ def test_has_method_in_file(self):
+ """Test has_method_in_file method."""
+ test_path = os.path.join(uc.TEST_DATA_DIR, 'class_file_path_testing',
+ 'hello_world_test.cc')
+ self.assertTrue(test_finder_utils.has_method_in_file(
+ test_path, frozenset(['PrintHelloWorld'])))
+ self.assertFalse(test_finder_utils.has_method_in_file(
+ test_path, frozenset(['PrintHelloWorld1'])))
+ test_path = os.path.join(uc.TEST_DATA_DIR, 'class_file_path_testing',
+ 'hello_world_test.java')
+ self.assertTrue(test_finder_utils.has_method_in_file(
+ test_path, frozenset(['testMethod1'])))
+ test_path = os.path.join(uc.TEST_DATA_DIR, 'class_file_path_testing',
+ 'hello_world_test.java')
+ self.assertTrue(test_finder_utils.has_method_in_file(
+ test_path, frozenset(['testMethod', 'testMethod2'])))
+ test_path = os.path.join(uc.TEST_DATA_DIR, 'class_file_path_testing',
+ 'hello_world_test.java')
+ self.assertFalse(test_finder_utils.has_method_in_file(
+ test_path, frozenset(['testMethod'])))
@mock.patch('__builtin__.raw_input', return_value='1')
def test_extract_test_from_tests(self, mock_input):
"""Test method extract_test_from_tests method."""
tests = []
self.assertEquals(test_finder_utils.extract_test_from_tests(tests), None)
- path = os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')
+ paths = [os.path.join(uc.ROOT, CLASS_DIR, uc.CLASS_NAME + '.java')]
unittest_utils.assert_strict_equal(
- self, test_finder_utils.extract_test_path(uc.FIND_ONE), path)
- path = os.path.join(uc.ROOT, OTHER_DIR, OTHER_CLASS_NAME)
+ self, test_finder_utils.extract_test_path(uc.FIND_ONE), paths)
+ paths = [os.path.join(uc.ROOT, OTHER_DIR, OTHER_CLASS_NAME)]
mock_input.return_value = '0'
unittest_utils.assert_strict_equal(
- self, test_finder_utils.extract_test_path(FIND_TWO), path)
+ self, test_finder_utils.extract_test_path(FIND_TWO), paths)
# Test inputing out-of-range integer or a string
mock_input.return_value = '100'
- self.assertEquals(test_finder_utils.extract_test_from_tests(uc.CLASS_NAME), None)
+ self.assertEquals(test_finder_utils.extract_test_from_tests(
+ uc.CLASS_NAME), [])
mock_input.return_value = 'lOO'
- self.assertEquals(test_finder_utils.extract_test_from_tests(uc.CLASS_NAME), None)
+ self.assertEquals(test_finder_utils.extract_test_from_tests(
+ uc.CLASS_NAME), [])
+
+ @mock.patch('__builtin__.raw_input', return_value='1')
+ def test_extract_test_from_multiselect(self, mock_input):
+ """Test method extract_test_from_tests method."""
+ # selecting 'All'
+ paths = ['/a/b/c.java', '/d/e/f.java', '/g/h/i.java']
+ mock_input.return_value = '3'
+ unittest_utils.assert_strict_equal(
+ self, sorted(test_finder_utils.extract_test_from_tests(
+ FIND_THREE_LIST)), sorted(paths))
+ # multi-select
+ paths = ['/a/b/c.java', '/g/h/i.java']
+ mock_input.return_value = '0,2'
+ unittest_utils.assert_strict_equal(
+ self, sorted(test_finder_utils.extract_test_from_tests(
+ FIND_THREE_LIST)), sorted(paths))
+ # selecting a range
+ paths = ['/d/e/f.java', '/g/h/i.java']
+ mock_input.return_value = '1-2'
+ unittest_utils.assert_strict_equal(
+ self, test_finder_utils.extract_test_from_tests(FIND_THREE_LIST), paths)
+ # mixed formats
+ paths = ['/a/b/c.java', '/d/e/f.java', '/g/h/i.java']
+ mock_input.return_value = '0,1-2'
+ unittest_utils.assert_strict_equal(
+ self, sorted(test_finder_utils.extract_test_from_tests(
+ FIND_THREE_LIST)), sorted(paths))
+ # input unsupported formats, return empty
+ paths = []
+ mock_input.return_value = '?/#'
+ unittest_utils.assert_strict_equal(
+ self, test_finder_utils.extract_test_path(FIND_THREE), paths)
@mock.patch('os.path.isdir')
def test_is_equal_or_sub_dir(self, mock_isdir):
@@ -358,13 +425,13 @@
def test_search_integration_dirs(self, mock_input):
"""Test search_integration_dirs."""
mock_input.return_value = '0'
- path = os.path.join(uc.ROOT, INT_DIR1, INT_FILE_NAME+'.xml')
+ paths = [os.path.join(uc.ROOT, INT_DIR1, INT_FILE_NAME+'.xml')]
int_dirs = [INT_DIR1]
test_result = test_finder_utils.search_integration_dirs(INT_FILE_NAME, int_dirs)
- unittest_utils.assert_strict_equal(self, test_result, path)
+ unittest_utils.assert_strict_equal(self, test_result, paths)
int_dirs = [INT_DIR1, INT_DIR2]
test_result = test_finder_utils.search_integration_dirs(INT_FILE_NAME, int_dirs)
- unittest_utils.assert_strict_equal(self, test_result, path)
+ unittest_utils.assert_strict_equal(self, test_result, paths)
@mock.patch('os.environ.get', return_value=uc.TEST_CONFIG_DATA_DIR)
@mock.patch('__builtin__.raw_input', return_value='0')
@@ -373,12 +440,12 @@
java_tmp_test_result = []
mock_input.return_value = '0'
java_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_TESTCASE_JAVA + '.java')
- java_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+ java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
uc.FIND_PATH_TESTCASE_JAVA))
mock_input.return_value = '1'
kt_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_TESTCASE_JAVA + '.kt')
- java_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+ java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
uc.FIND_PATH_TESTCASE_JAVA))
self.assertTrue(java_class in java_tmp_test_result)
@@ -387,10 +454,10 @@
del java_tmp_test_result[:]
mock_input.return_value = '0'
java_qualified_class = '{0}.{1}'.format(uc.FIND_PATH_FOLDER, uc.FIND_PATH_TESTCASE_JAVA)
- java_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+ java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
java_qualified_class))
mock_input.return_value = '1'
- java_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+ java_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
java_qualified_class))
self.assertTrue(java_class in java_tmp_test_result)
self.assertTrue(kt_class in java_tmp_test_result)
@@ -398,12 +465,12 @@
cc_tmp_test_result = []
mock_input.return_value = '0'
cpp_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_FILENAME_CC + '.cpp')
- cc_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+ cc_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
uc.FIND_PATH_TESTCASE_CC,
True))
mock_input.return_value = '1'
cc_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_FILENAME_CC + '.cc')
- cc_tmp_test_result.append(test_finder_utils.find_class_file(uc.FIND_PATH,
+ cc_tmp_test_result.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
uc.FIND_PATH_TESTCASE_CC,
True))
diff --git a/atest/test_finders/tf_integration_finder.py b/atest/test_finders/tf_integration_finder.py
index a96ad63..eecb9927 100644
--- a/atest/test_finders/tf_integration_finder.py
+++ b/atest/test_finders/tf_integration_finder.py
@@ -120,8 +120,10 @@
if not integration_name:
logging.warn('skipping <include> tag with no "name" value')
continue
- full_path = self._search_integration_dirs(integration_name)
- node = self._load_xml_file(full_path)
+ full_paths = self._search_integration_dirs(integration_name)
+ node = None
+ if full_paths:
+ node = self._load_xml_file(full_paths[0])
if node is None:
raise atest_error.FatalIncludeError("can't load %r" %
integration_name)
@@ -137,16 +139,17 @@
name: A string of integration name as seen in tf's list configs.
Returns:
- A string of test path if test found, else None.
+ A list of test path.
"""
+ test_files = []
for integration_dir in self.integration_dirs:
abs_path = os.path.join(self.root_dir, integration_dir)
- test_file = test_finder_utils.run_find_cmd(
+ found_test_files = test_finder_utils.run_find_cmd(
test_finder_utils.FIND_REFERENCE_TYPE.INTEGRATION,
abs_path, name)
- if test_file:
- return test_file
- return None
+ if found_test_files:
+ test_files.extend(found_test_files)
+ return test_files
def find_test_by_integration_name(self, name):
"""Find the test info matching the given integration name.
@@ -160,13 +163,17 @@
class_name = None
if ':' in name:
name, class_name = name.split(':')
- test_file = self._search_integration_dirs(name)
- if test_file is None:
+ test_files = self._search_integration_dirs(name)
+ if test_files is None:
return None
# Don't use names that simply match the path,
# must be the actual name used by TF to run the test.
- t_info = self._get_test_info(name, test_file, class_name)
- return t_info
+ t_infos = []
+ for test_file in test_files:
+ t_info = self._get_test_info(name, test_file, class_name)
+ if t_info:
+ t_infos.append(t_info)
+ return t_infos
def _get_test_info(self, name, test_file, class_name):
"""Find the test info matching the given test_file and class_name.
@@ -193,17 +200,24 @@
filters = frozenset()
if class_name:
class_name, methods = test_finder_utils.split_methods(class_name)
- if '.' not in class_name:
+ test_filters = []
+ if '.' in class_name:
+ test_filters.append(test_info.TestFilter(class_name, methods))
+ else:
logging.warn('Looking up fully qualified class name for: %s.'
'Improve speed by using fully qualified names.',
class_name)
- path = test_finder_utils.find_class_file(self.root_dir,
- class_name)
- if not path:
+ paths = test_finder_utils.find_class_file(self.root_dir,
+ class_name)
+ if not paths:
return None
- class_name = test_finder_utils.get_fully_qualified_class_name(
- path)
- filters = frozenset([test_info.TestFilter(class_name, methods)])
+ for path in paths:
+ class_name = (
+ test_finder_utils.get_fully_qualified_class_name(
+ path))
+ test_filters.append(test_info.TestFilter(
+ class_name, methods))
+ filters = frozenset(test_filters)
return test_info.TestInfo(
test_name=name,
test_runner=self._TEST_RUNNER,
@@ -223,7 +237,7 @@
path: A string of the test's path.
Returns:
- A populated TestInfo namedtuple if test found, else None
+ A list of populated TestInfo namedtuple if test found, else None
"""
path, _ = test_finder_utils.split_methods(path)
@@ -246,10 +260,10 @@
rel_config)
return None
int_name = match.group('int_name')
- return test_info.TestInfo(
+ return [test_info.TestInfo(
test_name=int_name,
test_runner=self._TEST_RUNNER,
build_targets=self._get_build_targets(rel_config),
data={constants.TI_REL_CONFIG: rel_config,
- constants.TI_FILTER: frozenset()})
+ constants.TI_FILTER: frozenset()})]
return None
diff --git a/atest/test_finders/tf_integration_finder_unittest.py b/atest/test_finders/tf_integration_finder_unittest.py
index 0a2cc84..a8b58cc 100755
--- a/atest/test_finders/tf_integration_finder_unittest.py
+++ b/atest/test_finders/tf_integration_finder_unittest.py
@@ -69,24 +69,25 @@
_fcqn, _build):
"""Test find_test_by_integration_name."""
mock_find.return_value = os.path.join(uc.ROOT, uc.INT_DIR, uc.INT_NAME + '.xml')
- t_info = self.tf_finder.find_test_by_integration_name(uc.INT_NAME)
- unittest_utils.assert_equal_testinfos(self, t_info, uc.INT_INFO)
- t_info = self.tf_finder.find_test_by_integration_name(INT_NAME_CLASS)
- unittest_utils.assert_equal_testinfos(self, t_info, INT_CLASS_INFO)
- t_info = self.tf_finder.find_test_by_integration_name(INT_NAME_METHOD)
- unittest_utils.assert_equal_testinfos(self, t_info, INT_METHOD_INFO)
+ t_infos = self.tf_finder.find_test_by_integration_name(uc.INT_NAME)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], uc.INT_INFO)
+ t_infos = self.tf_finder.find_test_by_integration_name(INT_NAME_CLASS)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], INT_CLASS_INFO)
+ t_infos = self.tf_finder.find_test_by_integration_name(INT_NAME_METHOD)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], INT_METHOD_INFO)
not_fully_qual = uc.INT_NAME + ':' + 'someClass'
- t_info = self.tf_finder.find_test_by_integration_name(not_fully_qual)
- unittest_utils.assert_equal_testinfos(self, t_info, INT_CLASS_INFO)
+ t_infos = self.tf_finder.find_test_by_integration_name(not_fully_qual)
+ unittest_utils.assert_equal_testinfos(self, t_infos[0], INT_CLASS_INFO)
mock_find.return_value = os.path.join(uc.ROOT, uc.GTF_INT_DIR,
uc.GTF_INT_NAME + '.xml')
+ t_infos = self.tf_finder.find_test_by_integration_name(uc.GTF_INT_NAME)
unittest_utils.assert_equal_testinfos(
self,
- self.tf_finder.find_test_by_integration_name(uc.GTF_INT_NAME),
+ t_infos[0],
uc.GTF_INT_INFO)
mock_find.return_value = ''
- self.assertIsNone(
- self.tf_finder.find_test_by_integration_name('NotIntName'))
+ self.assertEqual(
+ self.tf_finder.find_test_by_integration_name('NotIntName'), [])
@mock.patch.object(tf_integration_finder.TFIntegrationFinder,
'_get_build_targets', return_value=set())
@@ -100,21 +101,22 @@
_build):
"""Test find_int_test_by_path."""
path = os.path.join(uc.INT_DIR, uc.INT_NAME + '.xml')
+ t_infos = self.tf_finder.find_int_test_by_path(path)
unittest_utils.assert_equal_testinfos(
- self, uc.INT_INFO, self.tf_finder.find_int_test_by_path(path))
+ self, uc.INT_INFO, t_infos[0])
path = os.path.join(uc.GTF_INT_DIR, uc.GTF_INT_NAME + '.xml')
+ t_infos = self.tf_finder.find_int_test_by_path(path)
unittest_utils.assert_equal_testinfos(
- self, uc.GTF_INT_INFO, self.tf_finder.find_int_test_by_path(path))
+ self, uc.GTF_INT_INFO, t_infos[0])
#pylint: disable=protected-access
@mock.patch.object(tf_integration_finder.TFIntegrationFinder,
'_search_integration_dirs')
def test_load_xml_file(self, search):
"""Test _load_xml_file and _load_include_tags methods."""
- search.return_value = os.path.join(uc.TEST_DATA_DIR,
- 'CtsUiDeviceTestCases.xml')
+ search.return_value = [os.path.join(uc.TEST_DATA_DIR,
+ 'CtsUiDeviceTestCases.xml')]
xml_file = os.path.join(uc.TEST_DATA_DIR, constants.MODULE_CONFIG)
- print 'xml_file: %s' % xml_file
xml_root = self.tf_finder._load_xml_file(xml_file)
include_tags = xml_root.findall('.//include')
self.assertEqual(0, len(include_tags))
diff --git a/atest/test_runners/atest_tf_test_runner.py b/atest/test_runners/atest_tf_test_runner.py
index a4f62d5..ffd1ac9 100644
--- a/atest/test_runners/atest_tf_test_runner.py
+++ b/atest/test_runners/atest_tf_test_runner.py
@@ -500,6 +500,10 @@
if test_infos[0].from_test_mapping:
args.extend(constants.TEST_MAPPING_RESULT_SERVER_ARGS)
test_infos = self._flatten_test_infos(test_infos)
+ # In order to do dry-run verification, sort it to make each run has the
+ # same result
+ test_infos = list(test_infos)
+ test_infos.sort()
for info in test_infos:
args.extend([constants.TF_INCLUDE_FILTER, info.test_name])
diff --git a/atest/test_runners/robolectric_test_runner_unittest.py b/atest/test_runners/robolectric_test_runner_unittest.py
index 26e30f6..46164f0 100755
--- a/atest/test_runners/robolectric_test_runner_unittest.py
+++ b/atest/test_runners/robolectric_test_runner_unittest.py
@@ -66,11 +66,9 @@
json_event_data = json.dumps(event_data)
data = '%s %s\n\n' %(event_name, json_event_data)
- event_file = tempfile.NamedTemporaryFile(mode='w+r', delete=False)
- robo_proc = subprocess.Popen("echo '%s' >> %s && sleep %s"
- %(data,
- event_file.name,
- str(self.polling_time)), shell=True)
+ event_file = tempfile.NamedTemporaryFile(mode='w+r', delete=True)
+ subprocess.call("echo '%s' -n >> %s" %(data, event_file.name), shell=True)
+ robo_proc = subprocess.Popen("sleep %s" %str(self.polling_time * 2), shell=True)
self.suite_tr. _exec_with_robo_polling(event_file, robo_proc, mock_pe)
calls = [mock.call.process_event(event_name, event_data)]
mock_pe.assert_has_calls(calls)
@@ -105,11 +103,8 @@
'at FailureStrategy.fail(FailureStrategy.java:20)\n'}
data = '%s %s\n\n'%(event_name, json.dumps(event_data))
event_file = tempfile.NamedTemporaryFile(mode='w+r', delete=True)
- robo_proc = subprocess.Popen("echo '%s' >> %s && sleep %s"
- %(data,
- event_file.name,
- str(self.polling_time)),
- shell=True)
+ subprocess.call("echo '%s' -n >> %s" %(data, event_file.name), shell=True)
+ robo_proc = subprocess.Popen("sleep %s" %str(self.polling_time * 2), shell=True)
self.suite_tr. _exec_with_robo_polling(event_file, robo_proc, mock_pe)
calls = [mock.call.process_event(event_name, event_data)]
mock_pe.assert_has_calls(calls)
@@ -135,15 +130,12 @@
'testName':'someTestName2'}),
('TEST_RUN_ENDED', {}),
('TEST_MODULE_ENDED', {'foo': 'bar'}),]
- data_data = ''
+ data = ''
for event in events:
- data_data += '%s %s\n\n'%(event[0], json.dumps(event[1]))
+ data += '%s %s\n\n'%(event[0], json.dumps(event[1]))
- robo_proc = subprocess.Popen("echo '%s' >> %s && sleep %s"
- %(data_data,
- event_file.name,
- str(self.polling_time)),
- shell=True)
+ subprocess.call("echo '%s' -n >> %s" %(data, event_file.name), shell=True)
+ robo_proc = subprocess.Popen("sleep %s" %str(self.polling_time * 2), shell=True)
self.suite_tr. _exec_with_robo_polling(event_file, robo_proc, mock_pe)
calls = [mock.call.process_event(name, data) for name, data in events]
mock_pe.assert_has_calls(calls)
diff --git a/atest/tools/__init__.py b/atest/tools/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/tools/__init__.py
diff --git a/atest/tools/atest_updatedb.py b/atest/tools/atest_updatedb.py
new file mode 100755
index 0000000..6b0c8f8
--- /dev/null
+++ b/atest/tools/atest_updatedb.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Atest updatedb functions.
+"""
+
+from __future__ import print_function
+
+import logging
+import os
+import platform
+import shutil
+import subprocess
+
+AND_HOSTOUT_DIR = os.getenv('ANDROID_HOST_OUT', '')
+MAC_UPDB_SRC = os.path.join(os.path.dirname(__file__), 'updatedb_darwin.sh')
+MAC_UPDB_DST = os.path.join(AND_HOSTOUT_DIR, 'bin')
+UPDATEDB = 'updatedb'
+
+# updatedb does not support ".*" so below are excluded explicitly.
+_PRUNENAMES = ['.abc', '.appveyor', '.azure-pipelines',
+ '.bazelci', '.buildscript',
+ '.ci', '.circleci',
+ '.conan',
+ '.externalToolBuilders',
+ '.git', '.github', '.google', '.gradle',
+ '.idea', '.intermediates',
+ '.kokoro',
+ '.mvn',
+ '.prebuilt_info', '.private', '__pycache__',
+ '.repo',
+ '.semaphore', '.settings',
+ '.static', '.svn',
+ '.test', '.travis', '.tx',
+ '.vscode']
+_CACHE = 'locate.database'
+
+def _install_updatedb():
+ """Install a customized updatedb for MacOS."""
+ if platform.system() == 'Darwin':
+ if not os.path.isdir(MAC_UPDB_DST):
+ os.makedirs(MAC_UPDB_DST)
+ shutil.copy2(MAC_UPDB_SRC, os.path.join(MAC_UPDB_DST, UPDATEDB))
+ os.chmod(os.path.join(MAC_UPDB_DST, UPDATEDB), 0755)
+
+
+def run_updatedb(**kwargs):
+ """Run updatedb and generate cache in $ANDROID_HOST_OUT/locate.database
+
+ Args:
+ search_root: The path of the search root(-U).
+ prunepaths: A list of paths unwanted to be searched(-e).
+ prunenames: A list of dirname that won't be cached(-n).
+ output_cache: The filename of the updatedb cache(-o).
+
+ Returns:
+ Boolean of the status of updatedb execution, True if update successfully,
+ False otherwise.
+ """
+ repo_root = os.getenv('ANDROID_BUILD_TOP', '')
+ search_root = kwargs.get('search_root', repo_root)
+ prunepaths = kwargs.get('prunepaths', os.path.join(search_root, 'out'))
+ prunenames = kwargs.get('prunenames', ' '.join(_PRUNENAMES))
+ output_cache = kwargs.get('output_cache',
+ os.path.join(AND_HOSTOUT_DIR, _CACHE))
+ if not os.path.exists(os.path.dirname(output_cache)):
+ os.makedirs(os.path.dirname(output_cache))
+ updatedb_cmd = [UPDATEDB, '-l0']
+ updatedb_cmd.append('-U%s' % search_root)
+ updatedb_cmd.append('-e%s' % prunepaths)
+ updatedb_cmd.append('-n%s' % prunenames)
+ updatedb_cmd.append('-o%s' % output_cache)
+ try:
+ _install_updatedb()
+ except IOError as e:
+ logging.error('Error installing updatedb: %s', e)
+ return False
+ print('Running updatedb for locate...')
+ try:
+ full_env_vars = os.environ.copy()
+ logging.debug('Executing: %s', updatedb_cmd)
+ subprocess.check_call(updatedb_cmd, stderr=subprocess.STDOUT,
+ env=full_env_vars)
+ return True
+ except subprocess.CalledProcessError as err:
+ logging.error('Error executing: %s', updatedb_cmd)
+ if err.output:
+ logging.error(err.output)
+ return False
+
+if __name__ == '__main__':
+ run_updatedb()
diff --git a/atest/tools/atest_updatedb_unittest.py b/atest/tools/atest_updatedb_unittest.py
new file mode 100755
index 0000000..923c5dc
--- /dev/null
+++ b/atest/tools/atest_updatedb_unittest.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+#
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Unittest for atest_updatedb."""
+
+import os
+import platform
+import subprocess
+import sys
+import unittest
+
+import atest_updatedb
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+# pylint: disable=wrong-import-position
+import unittest_constants
+
+SEARCH_ROOT = unittest_constants.TEST_DATA_DIR
+PRUNEPATH = unittest_constants.TEST_CONFIG_DATA_DIR
+_CACHE = '/tmp/locate.database'
+
+
+class AtestUpdatedbUnittests(unittest.TestCase):
+ """"Unittest Class for atest_updatedb.py."""
+
+ def test_atest_updatedb(self):
+ """Test method run_updatedb."""
+ atest_updatedb.run_updatedb(search_root=SEARCH_ROOT,
+ prunepaths=PRUNEPATH,
+ output_cache=_CACHE)
+ # test_config/ is excluded so that a.xml won't be found.
+ locate_cmd1 = ['locate', '-d', _CACHE, '/a.xml']
+ # locate always return 0 when not found in Darwin, therefore,
+ # check null return in Darwin and return value in Linux.
+ if platform.system() == 'Darwin':
+ self.assertEqual(subprocess.check_output(locate_cmd1), "")
+ else:
+ self.assertEqual(subprocess.call(locate_cmd1), 1)
+ # module-info.json can be found in the search_root.
+ locate_cmd2 = ['locate', '-d', _CACHE, 'module-info.json']
+ self.assertEqual(subprocess.call(locate_cmd2), 0)
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/atest/tools/updatedb_darwin.sh b/atest/tools/updatedb_darwin.sh
new file mode 100755
index 0000000..9d621bc
--- /dev/null
+++ b/atest/tools/updatedb_darwin.sh
@@ -0,0 +1,111 @@
+#!/usr/bin/env bash
+#
+# Copyright 2019, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Warning and exit when failed to meet the requirements.
+[ "$(uname -s)" != "Darwin" ] && { echo "This program runs on Darwin only."; exit 0; }
+[ "$UID" -eq 0 ] && { echo "Running with root user is not supported."; exit 0; }
+
+function usage() {
+ echo "###########################################"
+ echo "Usage: $prog [-U|-e|-n|-o||-l|-f|-h]"
+ echo " -U: The PATH of the search root."
+ echo " -e: The PATH that unwanted to be searched."
+ echo " -n: The name of directories that won't be cached."
+ echo " -o: The PATH of the generated database."
+ echo " -l: No effect. For compatible with Linux mlocate."
+ echo " -f: Filesystems which should not search for."
+ echo " -h: This usage helper."
+ echo
+ echo "################ [EXAMPLE] ################"
+ echo "$prog -U \$ANDROID_BUILD_TOP -n .git -l 0 \\"
+ echo " -e \"\$ANDROID_BUILD_TOP/out \$ANDROID_BUILD_TOP/.repo\" \\"
+ echo " -o \"\$ANDROID_HOST_OUT/locate.database\""
+ echo
+ echo "locate -d \$ANDROID_HOST_OUT/locate.database atest.py"
+ echo "locate -d \$ANDROID_HOST_OUT/locate.database contrib/res/config"
+}
+
+function mktempdir() {
+ TMPDIR=/tmp
+ if ! TMPDIR=`mktemp -d $TMPDIR/locateXXXXXXXXXX`; then
+ exit 1
+ fi
+ temp=$TMPDIR/_updatedb$$
+}
+
+function _updatedb_main() {
+ # 0. Disable default features of bash.
+ set -o noglob # Disable * expension before passing arguments to find.
+ set -o errtrace # Sub-shells inherit error trap.
+
+ # 1. Get positional arguments and set variables.
+ prog=$(basename $0)
+ while getopts 'U:n:e:o:l:f:h' option; do
+ case $option in
+ U) SEARCHROOT="$OPTARG";; # Search root.
+ e) PRUNEPATHS="$OPTARG";; # Paths to be excluded.
+ n) PRUNENAMES="$OPTARG";; # Dirnames to be pruned.
+ o) DATABASE="$OPTARG";; # the output of the DB.
+ l) ;; # No effect.
+ f) PRUNEFS="$OPTARG";; # Disallow network filesystems.
+ *) usage; exit 0;;
+ esac
+ done
+
+ : ${SEARCHROOT:="$ANDROID_BUILD_TOP"}
+ if [ -z "$SEARCHROOT" ]; then
+ echo 'Either $SEARCHROOT or $ANDROID_BUILD_TOP is required.'
+ exit 0
+ fi
+
+ if [ -n "$ANDROID_BUILD_TOP" ]; then
+ PRUNEPATHS="$PRUNEPATHS $ANDROID_BUILD_TOP/out"
+ fi
+
+ PRUNENAMES="$PRUNENAMES *.class *.pyc .gitignore"
+ : ${DATABASE:=/tmp/locate.database}
+ : ${PRUNEFS:="nfs afp smb"}
+
+ # 2. Assemble excludes strings.
+ excludes=""
+ or=""
+ sortarg="-presort"
+ for fs in $PRUNEFS; do
+ excludes="$excludes $or -fstype $fs -prune"
+ or="-o"
+ done
+ for path in $PRUNEPATHS; do
+ excludes="$excludes $or -path $path -prune"
+ done
+ for file in $PRUNENAMES; do
+ excludes="$excludes $or -name $file -prune"
+ done
+
+ # 3. Find and create locate database.
+ # Delete $temp when trapping specified return values.
+ mktempdir
+ trap 'rm -rf $temp $TMPDIR; exit' 0 1 2 3 5 10 15
+ if find -s $SEARCHROOT $excludes $or -print 2>/dev/null -true |
+ /usr/libexec/locate.mklocatedb $sortarg > $temp 2>/dev/null; then
+ case x"`find $temp -size 257c -print`" in
+ x) cat $temp > $DATABASE;;
+ *) echo "$prog: database $temp is found empty."
+ exit 1;;
+ esac
+ fi
+}
+
+_updatedb_main "$@"
diff --git a/atest/unittest_constants.py b/atest/unittest_constants.py
index 0134618..03bf5c0 100644
--- a/atest/unittest_constants.py
+++ b/atest/unittest_constants.py
@@ -54,12 +54,21 @@
MODULE_BUILD_TARGETS = {'tradefed-core', MODULE_INFO_TARGET,
'MODULES-IN-%s' % MODULE_DIR.replace('/', '-'),
'module-specific-target'}
+MODULE_BUILD_TARGETS2 = {'build-target2'}
MODULE_DATA = {constants.TI_REL_CONFIG: CONFIG_FILE,
constants.TI_FILTER: frozenset()}
+MODULE_DATA2 = {constants.TI_REL_CONFIG: CONFIG_FILE,
+ constants.TI_FILTER: frozenset()}
MODULE_INFO = test_info.TestInfo(MODULE_NAME,
atf_tr.AtestTradefedTestRunner.NAME,
MODULE_BUILD_TARGETS,
MODULE_DATA)
+MODULE_INFO2 = test_info.TestInfo(MODULE2_NAME,
+ atf_tr.AtestTradefedTestRunner.NAME,
+ MODULE_BUILD_TARGETS2,
+ MODULE_DATA2)
+MODULE_INFOS = [MODULE_INFO]
+MODULE_INFOS2 = [MODULE_INFO, MODULE_INFO2]
CLASS_FILTER = test_info.TestFilter(FULL_CLASS_NAME, frozenset())
CLASS_DATA = {constants.TI_REL_CONFIG: CONFIG_FILE,
constants.TI_FILTER: frozenset([CLASS_FILTER])}
@@ -80,6 +89,17 @@
atf_tr.AtestTradefedTestRunner.NAME,
CLASS_BUILD_TARGETS,
CLASS_DATA)
+CLASS_INFOS = [CLASS_INFO]
+
+CLASS_BUILD_TARGETS2 = {'class-specific-target2'}
+CLASS_DATA2 = {constants.TI_REL_CONFIG: CONFIG_FILE,
+ constants.TI_FILTER: frozenset([CLASS_FILTER])}
+CLASS_INFO2 = test_info.TestInfo(MODULE2_NAME,
+ atf_tr.AtestTradefedTestRunner.NAME,
+ CLASS_BUILD_TARGETS2,
+ CLASS_DATA2)
+CLASS_INFOS = [CLASS_INFO]
+CLASS_INFOS2 = [CLASS_INFO, CLASS_INFO2]
PACKAGE_INFO = test_info.TestInfo(MODULE_NAME,
atf_tr.AtestTradefedTestRunner.NAME,
CLASS_BUILD_TARGETS,
@@ -160,10 +180,10 @@
CC_MODULE2_DIR = 'foo/bar/hello'
CC_MODULE2_NAME = 'hello_world_test'
CC_PATH = 'pf_test.cc'
-CC_FIND_ONE = ROOT + 'system/bt/hci/test/pf_test.cc:TEST_F(PFTest, test1) {\n' +\
- ROOT + 'system/bt/hci/test/pf_test.cc:TEST_F(PFTest, test2) {\n'
-CC_FIND_TWO = ROOT + 'other/dir/test.cpp:TEST(PFTest, test_f) {\n' +\
- ROOT + 'other/dir/test.cpp:TEST(PFTest, test_p) {\n'
+CC_FIND_ONE = ROOT + 'system/bt/hci/test/pf_test.cc:TEST_F(PFTest, test1) {\n' + \
+ ROOT + 'system/bt/hci/test/pf_test.cc:TEST_F(PFTest, test2) {\n'
+CC_FIND_TWO = ROOT + 'other/dir/test.cpp:TEST(PFTest, test_f) {\n' + \
+ ROOT + 'other/dir/test.cpp:TEST(PFTest, test_p) {\n'
CC_CONFIG2_FILE = os.path.join(CC_MODULE2_DIR, constants.MODULE_CONFIG)
CC_CLASS_FILTER = test_info.TestFilter(CC_CLASS_NAME+".*", frozenset())
CC_CLASS_DATA = {constants.TI_REL_CONFIG: CC_CONFIG_FILE,
@@ -175,7 +195,7 @@
CC_METHOD2_NAME = 'test2'
CC_METHOD_FILTER = test_info.TestFilter(CC_CLASS_NAME+"."+CC_METHOD_NAME,
frozenset())
-CC_METHOD2_FILTER = test_info.TestFilter(CC_CLASS_NAME+"."+CC_METHOD_NAME+\
+CC_METHOD2_FILTER = test_info.TestFilter(CC_CLASS_NAME+"."+CC_METHOD_NAME+ \
":"+CC_CLASS_NAME+"."+CC_METHOD2_NAME,
frozenset())
CC_METHOD_INFO = test_info.TestInfo(
diff --git a/atest/unittest_data/cache_root/cd66f9f5ad63b42d0d77a9334de6bb73.cache b/atest/unittest_data/cache_root/cd66f9f5ad63b42d0d77a9334de6bb73.cache
new file mode 100644
index 0000000..451a51e
--- /dev/null
+++ b/atest/unittest_data/cache_root/cd66f9f5ad63b42d0d77a9334de6bb73.cache
@@ -0,0 +1,72 @@
+c__builtin__
+set
+p0
+((lp1
+ccopy_reg
+_reconstructor
+p2
+(ctest_finders.test_info
+TestInfo
+p3
+c__builtin__
+object
+p4
+Ntp5
+Rp6
+(dp7
+S'install_locations'
+p8
+g0
+((lp9
+S'device'
+p10
+aS'host'
+p11
+atp12
+Rp13
+sS'test_runner'
+p14
+S'AtestTradefedTestRunner'
+p15
+sS'module_class'
+p16
+(lp17
+VNATIVE_TESTS
+p18
+asS'from_test_mapping'
+p19
+I00
+sS'build_targets'
+p20
+g0
+((lp21
+VMODULES-IN-platform_testing-tests-example-native
+p22
+atp23
+Rp24
+sg11
+I00
+sS'test_name'
+p25
+S'hello_world_test'
+p26
+sS'suite'
+p27
+NsS'data'
+p28
+(dp29
+S'rel_config'
+p30
+Vplatform_testing/tests/example/native/AndroidTest.xml
+p31
+sS'filter'
+p32
+c__builtin__
+frozenset
+p33
+((lp34
+tp35
+Rp36
+ssbatp37
+Rp38
+.
\ No newline at end of file
diff --git a/atest/unittest_data/class_file_path_testing/hello_world_test.java b/atest/unittest_data/class_file_path_testing/hello_world_test.java
index 8715753..8e0a999 100644
--- a/atest/unittest_data/class_file_path_testing/hello_world_test.java
+++ b/atest/unittest_data/class_file_path_testing/hello_world_test.java
@@ -1 +1,9 @@
package com.test.hello_world_test;
+
+public class HelloWorldTest {
+ @Test
+ public void testMethod1() throws Exception {}
+
+ @Test
+ public void testMethod2() throws Exception {}
+}
diff --git a/atest/unittest_data/test_mapping/folder6/test_mapping_sample_golden b/atest/unittest_data/test_mapping/folder6/test_mapping_sample_golden
new file mode 100644
index 0000000..db3998d
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder6/test_mapping_sample_golden
@@ -0,0 +1,14 @@
+{
+ "presubmit": [
+ {
+ "name": "test1",
+ "host": true,
+ "include-filter": "testClass#testMethod"
+ }
+ ],
+ "imports": [
+ {
+ "path": "path1//path2//path3"
+ }
+ ]
+}
diff --git a/atest/unittest_data/test_mapping/folder6/test_mapping_sample_with_comments b/atest/unittest_data/test_mapping/folder6/test_mapping_sample_with_comments
new file mode 100644
index 0000000..3f4083f
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder6/test_mapping_sample_with_comments
@@ -0,0 +1,16 @@
+{#comments1
+ "presubmit": [//comments2 // comments3 # comment4
+ #comments3
+ { #comments4
+ "name": "test1",#comments5
+//comments6
+ "host": true,//comments7
+ "include-filter": "testClass#testMethod" #comment11 // another comments
+ }#comments8
+ ],#comments9 // another comments
+ "imports": [
+ {
+ "path": "path1//path2//path3"#comment12
+ }
+ ]
+}#comments10
diff --git a/clearcut_client/Android.bp b/clearcut_client/Android.bp
new file mode 100644
index 0000000..6b9cbd9
--- /dev/null
+++ b/clearcut_client/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+ name: "tradefed-clearcut-client",
+ defaults: ["tradefed_defaults"],
+ srcs: [
+ "com/**/*.java",
+ ],
+ static_libs: [
+ "protobuf-java-util-prebuilt-jar",
+ ],
+ libs: [
+ "tradefed-protos",
+ "tradefed-common-util",
+ "devtools-annotations-prebuilt",
+ ],
+}
diff --git a/clearcut_client/OWNERS b/clearcut_client/OWNERS
new file mode 100644
index 0000000..01ac8d9
--- /dev/null
+++ b/clearcut_client/OWNERS
@@ -0,0 +1,3 @@
+# Base Owners + extra folks familiar with clearcut and can help reviewing it
+kellyhung@google.com
+yangbill@google.com
diff --git a/clearcut_client/README.md b/clearcut_client/README.md
new file mode 100644
index 0000000..ecac943
--- /dev/null
+++ b/clearcut_client/README.md
@@ -0,0 +1,5 @@
+# Trade Federation Clearcut Client
+
+A Tradefed component for our user metrics collection client.
+
+This directory should only contain classes related to clearcut.
diff --git a/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java b/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java
new file mode 100644
index 0000000..ee1d573
--- /dev/null
+++ b/clearcut_client/com/android/tradefed/clearcut/ClearcutClient.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.asuite.clearcut.Clientanalytics.ClientInfo;
+import com.android.asuite.clearcut.Clientanalytics.LogEvent;
+import com.android.asuite.clearcut.Clientanalytics.LogRequest;
+import com.android.asuite.clearcut.Clientanalytics.LogResponse;
+import com.android.asuite.clearcut.Common.UserType;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.net.HttpHelper;
+
+import com.google.protobuf.util.JsonFormat;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/** Client that allows reporting usage metrics to clearcut. */
+public class ClearcutClient {
+
+ public static final String DISABLE_CLEARCUT_KEY = "DISABLE_CLEARCUT";
+
+ private static final String CLEARCUT_PROD_URL = "https://play.googleapis.com/log";
+ private static final int CLIENT_TYPE = 1;
+ private static final int INTERNAL_LOG_SOURCE = 971;
+ private static final int EXTERNAL_LOG_SOURCE = 934;
+
+ private static final long SCHEDULER_INITIAL_DELAY_SECONDS = 2;
+ private static final long SCHEDULER_PERDIOC_SECONDS = 30;
+
+ private static final String GOOGLE_EMAIL = "@google.com";
+ private static final String GOOGLE_HOSTNAME = ".google.com";
+
+ private File mCachedUuidFile = new File(System.getProperty("user.home"), ".tradefed");
+ private String mRunId;
+
+ private final int mLogSource;
+ private final String mUrl;
+ private final UserType mUserType;
+
+ // Consider synchronized list
+ private List<LogRequest> mExternalEventQueue;
+ // The pool executor to actually post the metrics
+ private ScheduledThreadPoolExecutor mExecutor;
+ // Whether the clearcut client should be inop
+ private boolean mDisabled = false;
+
+ public ClearcutClient() {
+ this(null);
+ }
+
+ /**
+ * Create Client with customized posting URL and forcing whether it's internal or external user.
+ */
+ @VisibleForTesting
+ protected ClearcutClient(String url) {
+ mDisabled = isClearcutDisabled();
+
+ // We still have to set the 'final' variable so go through the assignments before returning
+ if (!mDisabled && isGoogleUser()) {
+ mLogSource = INTERNAL_LOG_SOURCE;
+ mUserType = UserType.GOOGLE;
+ } else {
+ mLogSource = EXTERNAL_LOG_SOURCE;
+ mUserType = UserType.EXTERNAL;
+ }
+ if (url == null) {
+ mUrl = CLEARCUT_PROD_URL;
+ } else {
+ mUrl = url;
+ }
+ mRunId = UUID.randomUUID().toString();
+ mExternalEventQueue = new ArrayList<>();
+
+ if (mDisabled) {
+ return;
+ }
+
+ // Print the notice
+ System.out.println(NoticeMessageUtil.getNoticeMessage(mUserType));
+
+ // Executor to actually send the events.
+ mExecutor = new ScheduledThreadPoolExecutor(1);
+ Runnable command =
+ new Runnable() {
+ @Override
+ public void run() {
+ flushEvents();
+ }
+ };
+ mExecutor.scheduleAtFixedRate(
+ command,
+ SCHEDULER_INITIAL_DELAY_SECONDS,
+ SCHEDULER_PERDIOC_SECONDS,
+ TimeUnit.SECONDS);
+ }
+
+ /** Send the first event to notify that Tradefed was started. */
+ public void notifyTradefedStartEvent() {
+ if (mDisabled) {
+ return;
+ }
+ LogRequest.Builder request = createBaseLogRequest();
+ LogEvent.Builder logEvent = LogEvent.newBuilder();
+ logEvent.setEventTimeMs(System.currentTimeMillis());
+ logEvent.setSourceExtension(
+ ClearcutEventHelper.createStartEvent(getGroupingKey(), mRunId, mUserType));
+ request.addLogEvent(logEvent);
+ queueEvent(request.build());
+ }
+
+ /** Stop the periodic sending of clearcut events */
+ public void stop() {
+ if (mExecutor != null) {
+ mExecutor.setRemoveOnCancelPolicy(true);
+ mExecutor.shutdown();
+ mExecutor = null;
+ }
+ // Send all remaining events
+ flushEvents();
+ }
+
+ /** Add an event to the queue of events that needs to be send. */
+ public void queueEvent(LogRequest event) {
+ synchronized (mExternalEventQueue) {
+ mExternalEventQueue.add(event);
+ }
+ }
+
+ /** Returns the current queue size. */
+ public final int getQueueSize() {
+ synchronized (mExternalEventQueue) {
+ return mExternalEventQueue.size();
+ }
+ }
+
+ /** Allows to override the default cached uuid file. */
+ public void setCachedUuidFile(File uuidFile) {
+ mCachedUuidFile = uuidFile;
+ }
+
+ /** Get a new or the cached uuid for the user. */
+ @VisibleForTesting
+ String getGroupingKey() {
+ String uuid = null;
+ if (mCachedUuidFile.exists()) {
+ try {
+ uuid = FileUtil.readStringFromFile(mCachedUuidFile);
+ } catch (IOException e) {
+ CLog.e(e);
+ }
+ }
+ if (uuid == null || uuid.isEmpty()) {
+ uuid = UUID.randomUUID().toString();
+ try {
+ FileUtil.writeToFile(uuid, mCachedUuidFile);
+ } catch (IOException e) {
+ CLog.e(e);
+ }
+ }
+ return uuid;
+ }
+
+ /** Returns True if clearcut is disabled, False otherwise. */
+ @VisibleForTesting
+ boolean isClearcutDisabled() {
+ return "1".equals(System.getenv(DISABLE_CLEARCUT_KEY));
+ }
+
+ /** Returns True if the user is a Googler, False otherwise. */
+ @VisibleForTesting
+ boolean isGoogleUser() {
+ CommandResult gitRes =
+ RunUtil.getDefault()
+ .runTimedCmdSilently(60000L, "git", "config", "--get", "user.email");
+ if (CommandStatus.SUCCESS.equals(gitRes.getStatus())) {
+ String stdout = gitRes.getStdout();
+ if (stdout != null && stdout.trim().endsWith(GOOGLE_EMAIL)) {
+ return true;
+ }
+ }
+ try {
+ String hostname = InetAddress.getLocalHost().getHostName();
+ if (hostname.contains(GOOGLE_HOSTNAME)) {
+ return true;
+ }
+ } catch (UnknownHostException e) {
+ // Ignore
+ }
+ return false;
+ }
+
+ private LogRequest.Builder createBaseLogRequest() {
+ LogRequest.Builder request = LogRequest.newBuilder();
+ request.setLogSource(mLogSource);
+ request.setClientInfo(ClientInfo.newBuilder().setClientType(CLIENT_TYPE));
+ return request;
+ }
+
+ private void flushEvents() {
+ List<LogRequest> copy = new ArrayList<>();
+ synchronized (mExternalEventQueue) {
+ copy.addAll(mExternalEventQueue);
+ mExternalEventQueue.clear();
+ }
+ while (!copy.isEmpty()) {
+ LogRequest event = copy.remove(0);
+ sendToClearcut(event);
+ }
+ }
+
+ /** Send one event to the configured server. */
+ private void sendToClearcut(LogRequest event) {
+ HttpHelper helper = new HttpHelper();
+
+ InputStream inputStream = null;
+ InputStream errorStream = null;
+ OutputStream outputStream = null;
+ OutputStreamWriter outputStreamWriter = null;
+ try {
+ HttpURLConnection connection = helper.createConnection(new URL(mUrl), "POST", "text");
+ outputStream = connection.getOutputStream();
+ outputStreamWriter = new OutputStreamWriter(outputStream);
+
+ String jsonObject = JsonFormat.printer().preservingProtoFieldNames().print(event);
+ outputStreamWriter.write(jsonObject.toString());
+ outputStreamWriter.flush();
+
+ inputStream = connection.getInputStream();
+ LogResponse response = LogResponse.parseFrom(inputStream);
+
+ errorStream = connection.getErrorStream();
+ if (errorStream != null) {
+ String message = StreamUtil.getStringFromStream(errorStream);
+ CLog.e("Error posting clearcut event: '%s'. LogResponse: '%s'", message, response);
+ }
+ } catch (IOException e) {
+ CLog.e(e);
+ } finally {
+ StreamUtil.close(outputStream);
+ StreamUtil.close(inputStream);
+ StreamUtil.close(outputStreamWriter);
+ StreamUtil.close(errorStream);
+ }
+ }
+}
diff --git a/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java b/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java
new file mode 100644
index 0000000..e8a6ab334
--- /dev/null
+++ b/clearcut_client/com/android/tradefed/clearcut/ClearcutEventHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+import com.android.asuite.clearcut.Common.UserType;
+import com.android.asuite.clearcut.ExternalUserLog.AtestLogEventExternal;
+import com.android.asuite.clearcut.ExternalUserLog.AtestLogEventExternal.AtestStartEvent;
+import com.android.asuite.clearcut.InternalUserLog.AtestLogEventInternal;
+
+import com.google.protobuf.ByteString;
+
+/** Utility to help populate the event protos */
+public class ClearcutEventHelper {
+
+ private static final String TOOL_NAME = "Tradefed";
+
+ /**
+ * Create the start event for Tradefed.
+ *
+ * @param userKey The unique id representing the user
+ * @param runId The current id for the session.
+ * @param userType The type of the user: internal or external.
+ * @return a ByteString representation of the even proto.
+ */
+ public static ByteString createStartEvent(String userKey, String runId, UserType userType) {
+ if (UserType.GOOGLE.equals(userType)) {
+ AtestLogEventInternal.Builder builder =
+ createBaseInternalEventBuilder(userKey, runId, userType);
+ AtestLogEventInternal.AtestStartEvent.Builder startEventBuilder =
+ AtestLogEventInternal.AtestStartEvent.newBuilder();
+ builder.setAtestStartEvent(startEventBuilder.build());
+ return builder.build().toByteString();
+ }
+
+ AtestLogEventExternal.Builder builder =
+ createBaseExternalEventBuilder(userKey, runId, userType);
+ AtestStartEvent.Builder startBuilder = AtestStartEvent.newBuilder();
+ builder.setAtestStartEvent(startBuilder.build());
+ return builder.build().toByteString();
+ }
+
+ /**
+ * Create the basic event builder with all the common informations.
+ *
+ * @param userKey The unique id representing the user
+ * @param runId The current id for the session.
+ * @param userType The type of the user: internal or external.
+ * @return a builder for the event.
+ */
+ private static AtestLogEventExternal.Builder createBaseExternalEventBuilder(
+ String userKey, String runId, UserType userType) {
+ AtestLogEventExternal.Builder builder = AtestLogEventExternal.newBuilder();
+ builder.setUserKey(userKey);
+ builder.setRunId(runId);
+ builder.setUserType(userType);
+ builder.setToolName(TOOL_NAME);
+ return builder;
+ }
+
+ /**
+ * Create the basic event builder with all the common informations.
+ *
+ * @param userKey The unique id representing the user
+ * @param runId The current id for the session.
+ * @param userType The type of the user: internal or external.
+ * @return a builder for the event.
+ */
+ private static AtestLogEventInternal.Builder createBaseInternalEventBuilder(
+ String userKey, String runId, UserType userType) {
+ AtestLogEventInternal.Builder builder = AtestLogEventInternal.newBuilder();
+ builder.setUserKey(userKey);
+ builder.setRunId(runId);
+ builder.setUserType(userType);
+ builder.setToolName(TOOL_NAME);
+ return builder;
+ }
+}
diff --git a/clearcut_client/com/android/tradefed/clearcut/NoticeMessageUtil.java b/clearcut_client/com/android/tradefed/clearcut/NoticeMessageUtil.java
new file mode 100644
index 0000000..75cf601
--- /dev/null
+++ b/clearcut_client/com/android/tradefed/clearcut/NoticeMessageUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+import com.android.asuite.clearcut.Common.UserType;
+
+/** Utility to create the notice message. */
+public class NoticeMessageUtil {
+
+ private static final String INTERNAL_AGREEMENT = "https://cla.developers.google.com/";
+ private static final String EXTERNAL_AGREEMENT = "https://opensource.google.com/docs/cla/";
+ private static final String ANONYMOUS = "anonymous ";
+
+ private static final String NOTICE_MESSAGE =
+ "==================\nNotice:\n"
+ + "We collect %susage statistics in accordance with our Content Licenses "
+ + "(https://source.android.com/setup/start/licenses), Contributor License "
+ + "Agreement (%s), Privacy Policy "
+ + "(https://policies.google.com/privacy) and Terms of Service "
+ + "(https://policies.google.com/terms)."
+ + "\n==================";
+
+ private NoticeMessageUtil() {}
+
+ /** Returns the notice message based on the user type (internal vs external). */
+ public static String getNoticeMessage(UserType type) {
+ if (UserType.EXTERNAL.equals(type)) {
+ return String.format(NOTICE_MESSAGE, ANONYMOUS, EXTERNAL_AGREEMENT);
+ } else {
+ return String.format(NOTICE_MESSAGE, "", INTERNAL_AGREEMENT);
+ }
+ }
+}
diff --git a/clearcut_client/com/android/tradefed/clearcut/TerminateClearcutClient.java b/clearcut_client/com/android/tradefed/clearcut/TerminateClearcutClient.java
new file mode 100644
index 0000000..d5f6204
--- /dev/null
+++ b/clearcut_client/com/android/tradefed/clearcut/TerminateClearcutClient.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+/** Thread that allows to ensure client has been stop via {@link Runtime}. */
+public class TerminateClearcutClient extends Thread {
+
+ private final ClearcutClient mClient;
+
+ public TerminateClearcutClient(ClearcutClient client) {
+ mClient = client;
+ }
+
+ @Override
+ public void run() {
+ // TODO: report the exit event if not already reported
+ mClient.stop();
+ }
+}
diff --git a/common_util/Android.bp b/common_util/Android.bp
new file mode 100644
index 0000000..9457e29
--- /dev/null
+++ b/common_util/Android.bp
@@ -0,0 +1,31 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+ name: "tradefed-common-util",
+ defaults: ["tradefed_defaults"],
+ srcs: [
+ "com/**/*.java",
+ ],
+ static_libs: [
+ "commons-compress-prebuilt",
+ ],
+ libs: [
+ "ddmlib-prebuilt",
+ "guava",
+ "tradefed-protos",
+ "devtools-annotations-prebuilt",
+ ],
+}
+
diff --git a/common_util/OWNERS b/common_util/OWNERS
new file mode 100644
index 0000000..19b5a68
--- /dev/null
+++ b/common_util/OWNERS
@@ -0,0 +1,4 @@
+# Base Owners + extra folks that can review common_util
+allenhair@google.com
+bettyzhou@google.com
+mrosenfeld@google.com
diff --git a/common_util/README.md b/common_util/README.md
new file mode 100644
index 0000000..ac50fd8
--- /dev/null
+++ b/common_util/README.md
@@ -0,0 +1,9 @@
+# Trade Federation Common Util
+
+Set of utilities and classes that are shared across TF components.
+
+This directory should contain classes that are:
+* Providing a generic service.
+* A shared data representation.
+
+
diff --git a/src/com/android/tradefed/build/BuildSerializedVersion.java b/common_util/com/android/tradefed/build/BuildSerializedVersion.java
similarity index 100%
rename from src/com/android/tradefed/build/BuildSerializedVersion.java
rename to common_util/com/android/tradefed/build/BuildSerializedVersion.java
diff --git a/src/com/android/tradefed/command/CommandInterrupter.java b/common_util/com/android/tradefed/command/CommandInterrupter.java
similarity index 100%
rename from src/com/android/tradefed/command/CommandInterrupter.java
rename to common_util/com/android/tradefed/command/CommandInterrupter.java
diff --git a/src/com/android/tradefed/command/FatalHostError.java b/common_util/com/android/tradefed/command/FatalHostError.java
similarity index 100%
rename from src/com/android/tradefed/command/FatalHostError.java
rename to common_util/com/android/tradefed/command/FatalHostError.java
diff --git a/src/com/android/tradefed/config/ConfigurationException.java b/common_util/com/android/tradefed/config/ConfigurationException.java
similarity index 100%
rename from src/com/android/tradefed/config/ConfigurationException.java
rename to common_util/com/android/tradefed/config/ConfigurationException.java
diff --git a/src/com/android/tradefed/config/Option.java b/common_util/com/android/tradefed/config/Option.java
similarity index 96%
rename from src/com/android/tradefed/config/Option.java
rename to common_util/com/android/tradefed/config/Option.java
index 7004dfa..736e990 100644
--- a/src/com/android/tradefed/config/Option.java
+++ b/common_util/com/android/tradefed/config/Option.java
@@ -120,4 +120,10 @@
* ignored completely for options that are {@link Collection}s or {@link Map}s.
*/
OptionUpdateRule updateRule() default OptionUpdateRule.LAST;
+
+ /**
+ * Internal Only - Do not set. Specify whether the field value was changed from its default
+ * value or not.
+ */
+ boolean isChanged() default false;
}
diff --git a/src/com/android/tradefed/config/OptionClass.java b/common_util/com/android/tradefed/config/OptionClass.java
similarity index 100%
rename from src/com/android/tradefed/config/OptionClass.java
rename to common_util/com/android/tradefed/config/OptionClass.java
diff --git a/common_util/com/android/tradefed/config/OptionDef.java b/common_util/com/android/tradefed/config/OptionDef.java
new file mode 100644
index 0000000..b6f0a03
--- /dev/null
+++ b/common_util/com/android/tradefed/config/OptionDef.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config;
+
+import com.android.tradefed.build.BuildSerializedVersion;
+
+import java.io.Serializable;
+
+/** Holds the details of an {@link Option}. */
+public final class OptionDef implements Serializable {
+ private static final long serialVersionUID = BuildSerializedVersion.VERSION;
+
+ public final String name;
+ public final String key;
+ public final String value;
+ public final String source;
+ public final String applicableObjectType;
+
+ public OptionDef(String optionName, String optionValue, String source) {
+ this(optionName, null, optionValue, source, null);
+ }
+
+ public OptionDef(String optionName, String optionKey, String optionValue, String source) {
+ this(optionName, optionKey, optionValue, source, null);
+ }
+
+ public OptionDef(
+ String optionName, String optionKey, String optionValue, String source, String type) {
+ this.name = optionName;
+ this.key = optionKey;
+ this.value = optionValue;
+ this.source = source;
+ this.applicableObjectType = type;
+ }
+}
diff --git a/src/com/android/tradefed/config/OptionUpdateRule.java b/common_util/com/android/tradefed/config/OptionUpdateRule.java
similarity index 100%
rename from src/com/android/tradefed/config/OptionUpdateRule.java
rename to common_util/com/android/tradefed/config/OptionUpdateRule.java
diff --git a/src/com/android/tradefed/log/LogUtil.java b/common_util/com/android/tradefed/log/LogUtil.java
similarity index 90%
rename from src/com/android/tradefed/log/LogUtil.java
rename to common_util/com/android/tradefed/log/LogUtil.java
index 1ae13c3..5ae0f84 100644
--- a/src/com/android/tradefed/log/LogUtil.java
+++ b/common_util/com/android/tradefed/log/LogUtil.java
@@ -18,8 +18,6 @@
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
-import com.android.tradefed.config.GlobalConfiguration;
-import com.android.tradefed.config.IGlobalConfiguration;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -68,7 +66,6 @@
public static class CLog {
protected static final String CLASS_NAME = CLog.class.getName();
- private static IGlobalConfiguration sGlobalConfig = null;
/**
* The shim version of {@link Log#v(String, String)}.
@@ -264,11 +261,6 @@
* @param t (Optional) An exception to log. If null, only message will be logged.
*/
public static void wtf(String message, Throwable t) {
- ITerribleFailureHandler wtfHandler = getGlobalConfigInstance().getWtfHandler();
-
- /* since wtf(String, Throwable) can be called directly or through an overloaded
- * method, ie wtf(String), the stack trace frame of the external class name that
- * called CLog can vary, so we use findCallerClassName to find it */
String tag = findCallerClassName();
String logMessage = "WTF - " + message;
String stackTrace = getStackTraceString(t);
@@ -277,31 +269,6 @@
}
Log.logAndDisplay(LogLevel.ASSERT, tag, logMessage);
- if (wtfHandler != null) {
- wtfHandler.onTerribleFailure(message, t);
- }
- }
-
- /**
- * Sets the GlobalConfiguration instance for CLog to use - exposed for unit testing
- *
- * @param globalConfig the GlobalConfiguration object for CLog to use
- */
- // @VisibleForTesting
- public static void setGlobalConfigInstance(IGlobalConfiguration globalConfig) {
- sGlobalConfig = globalConfig;
- }
-
- /**
- * Gets the GlobalConfiguration instance, useful for unit testing
- *
- * @return the GlobalConfiguration singleton instance
- */
- private static IGlobalConfiguration getGlobalConfigInstance() {
- if (sGlobalConfig == null) {
- sGlobalConfig = GlobalConfiguration.getInstance();
- }
- return sGlobalConfig;
}
/**
diff --git a/src/com/android/tradefed/result/ByteArrayInputStreamSource.java b/common_util/com/android/tradefed/result/ByteArrayInputStreamSource.java
similarity index 100%
rename from src/com/android/tradefed/result/ByteArrayInputStreamSource.java
rename to common_util/com/android/tradefed/result/ByteArrayInputStreamSource.java
diff --git a/src/com/android/tradefed/result/FileInputStreamSource.java b/common_util/com/android/tradefed/result/FileInputStreamSource.java
similarity index 100%
rename from src/com/android/tradefed/result/FileInputStreamSource.java
rename to common_util/com/android/tradefed/result/FileInputStreamSource.java
diff --git a/src/com/android/tradefed/result/InputStreamSource.java b/common_util/com/android/tradefed/result/InputStreamSource.java
similarity index 100%
rename from src/com/android/tradefed/result/InputStreamSource.java
rename to common_util/com/android/tradefed/result/InputStreamSource.java
diff --git a/src/com/android/tradefed/result/LogDataType.java b/common_util/com/android/tradefed/result/LogDataType.java
similarity index 96%
rename from src/com/android/tradefed/result/LogDataType.java
rename to common_util/com/android/tradefed/result/LogDataType.java
index 9589c90..2024bf1 100644
--- a/src/com/android/tradefed/result/LogDataType.java
+++ b/common_util/com/android/tradefed/result/LogDataType.java
@@ -27,6 +27,8 @@
MP4("mp4", "video/mp4", true, false),
EAR("ear", "application/octet-stream", true, false),
ZIP("zip", "application/zip", true, false),
+ SEVEN_Z("7z", "application/x-7z-compressed", true, false),
+ BITS("bits", "application/octet-stream", true, false),
JPEG("jpeg", "image/jpeg", true, false),
TAR_GZ("tar.gz", "application/gzip", true, false),
GZIP("gz", "application/gzip", true, false),
diff --git a/src/com/android/tradefed/testtype/Abi.java b/common_util/com/android/tradefed/testtype/Abi.java
similarity index 100%
rename from src/com/android/tradefed/testtype/Abi.java
rename to common_util/com/android/tradefed/testtype/Abi.java
diff --git a/src/com/android/tradefed/testtype/IAbi.java b/common_util/com/android/tradefed/testtype/IAbi.java
similarity index 100%
rename from src/com/android/tradefed/testtype/IAbi.java
rename to common_util/com/android/tradefed/testtype/IAbi.java
diff --git a/src/com/android/tradefed/util/AbiUtils.java b/common_util/com/android/tradefed/util/AbiUtils.java
similarity index 100%
rename from src/com/android/tradefed/util/AbiUtils.java
rename to common_util/com/android/tradefed/util/AbiUtils.java
diff --git a/src/com/android/tradefed/util/ArrayUtil.java b/common_util/com/android/tradefed/util/ArrayUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/ArrayUtil.java
rename to common_util/com/android/tradefed/util/ArrayUtil.java
diff --git a/src/com/android/tradefed/util/ByteArrayList.java b/common_util/com/android/tradefed/util/ByteArrayList.java
similarity index 100%
rename from src/com/android/tradefed/util/ByteArrayList.java
rename to common_util/com/android/tradefed/util/ByteArrayList.java
diff --git a/common_util/com/android/tradefed/util/ByteArrayUtil.java b/common_util/com/android/tradefed/util/ByteArrayUtil.java
new file mode 100644
index 0000000..510321b
--- /dev/null
+++ b/common_util/com/android/tradefed/util/ByteArrayUtil.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Utilities to operate on byte array, e.g., convert bytes to integer.
+ *
+ * <p>Java doesn't have an unsigned value type, so expansion is needed to convert an unsigned
+ * integer stored in 4 bytes to a long value, or unsigned short stored in 2 bytes to an integer
+ * value.
+ */
+public class ByteArrayUtil {
+
+ /**
+ * Get a {@link ByteBuffer} for the given bytes wrapped in a byte array of given size.
+ *
+ * <p>java doesn't have an unsigned value type, so expansion is needed to convert an unsigned
+ * short stored in 2 bytes to an integer value.
+ *
+ * @param bytes an array of bytes.
+ * @param offset the start offset of the integer data.
+ * @param length the length of the integer data.
+ * @param containerSize the size of the array to store the given bytes, append zero to unfilled
+ * items.
+ * @return a {@link ByteBuffer} for the given bytes wrapped in a byte array of given size.
+ */
+ private static ByteBuffer getByteBuffer(
+ byte[] bytes, int offset, int length, int containerSize) {
+ byte[] data = new byte[containerSize];
+ for (int i = 0; i < length; i++) {
+ data[i] = bytes[offset + i];
+ }
+ return ByteBuffer.wrap(data).order(java.nio.ByteOrder.LITTLE_ENDIAN);
+ }
+
+ /**
+ * Get an integer from the given bytes.
+ *
+ * <p>java doesn't have an unsigned value type, so expansion is needed to convert an unsigned
+ * short stored in 2 bytes to an integer value.
+ *
+ * @param bytes an array of bytes.
+ * @param offset the start offset of the integer data.
+ * @param length the length of the integer data.
+ * @return an int value from the given bytes.
+ */
+ public static int getInt(byte[] bytes, int offset, int length) {
+ return getByteBuffer(bytes, offset, length, 4).getInt();
+ }
+
+ /**
+ * Get a long value from the given bytes.
+ *
+ * <p>java doesn't have an unsigned value type, so expansion is needed to convert an unsigned
+ * integer stored in 4 bytes to a long value.
+ *
+ * @param bytes an array of bytes.
+ * @param offset the start offset of the long value.
+ * @param length the length of the long value.
+ * @return a long value from the given bytes.
+ */
+ public static long getLong(byte[] bytes, int offset, int length) {
+ return getByteBuffer(bytes, offset, length, 8).getLong();
+ }
+
+ /**
+ * Get the string from the given bytes.
+ *
+ * @param bytes an array of bytes.
+ * @param offset the start offset of the string data.
+ * @param length the length of the string data.
+ */
+ public static String getString(byte[] bytes, int offset, int length) {
+ return new String(Arrays.copyOfRange(bytes, offset, offset + length));
+ }
+}
diff --git a/src/com/android/tradefed/util/CommandResult.java b/common_util/com/android/tradefed/util/CommandResult.java
similarity index 100%
rename from src/com/android/tradefed/util/CommandResult.java
rename to common_util/com/android/tradefed/util/CommandResult.java
diff --git a/src/com/android/tradefed/util/CommandStatus.java b/common_util/com/android/tradefed/util/CommandStatus.java
similarity index 100%
rename from src/com/android/tradefed/util/CommandStatus.java
rename to common_util/com/android/tradefed/util/CommandStatus.java
diff --git a/src/com/android/tradefed/util/Email.java b/common_util/com/android/tradefed/util/Email.java
similarity index 100%
rename from src/com/android/tradefed/util/Email.java
rename to common_util/com/android/tradefed/util/Email.java
diff --git a/src/com/android/tradefed/util/FileUtil.java b/common_util/com/android/tradefed/util/FileUtil.java
similarity index 96%
rename from src/com/android/tradefed/util/FileUtil.java
rename to common_util/com/android/tradefed/util/FileUtil.java
index 9d919e9..2034972 100644
--- a/src/com/android/tradefed/util/FileUtil.java
+++ b/common_util/com/android/tradefed/util/FileUtil.java
@@ -1,3 +1,4 @@
+package com.android.tradefed.util;
/*
* Copyright (C) 2010 The Android Open Source Project
*
@@ -13,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.tradefed.util;
+
import com.android.ddmlib.Log;
import com.android.tradefed.command.FatalHostError;
@@ -615,12 +616,29 @@
*/
public static void writeToFile(
InputStream input, File destFile, boolean append) throws IOException {
+ // Set size to a negative value to write all content starting at the given offset.
+ writeToFile(input, destFile, append, 0, -1);
+ }
+
+ /**
+ * A helper method for writing stream data to file
+ *
+ * @param input the unbuffered input stream
+ * @param destFile the destination file to write or append to
+ * @param append append to end of file if true, overwrite otherwise
+ * @param startOffset the start offset of the input stream to retrieve data
+ * @param size number of bytes to retrieve from the input stream, set it to a negative value to
+ * retrieve all content starting at the given offset.
+ */
+ public static void writeToFile(
+ InputStream input, File destFile, boolean append, long startOffset, long size)
+ throws IOException {
InputStream origStream = null;
OutputStream destStream = null;
try {
origStream = new BufferedInputStream(input);
destStream = new BufferedOutputStream(new FileOutputStream(destFile, append));
- StreamUtil.copyStreams(origStream, destStream);
+ StreamUtil.copyStreams(origStream, destStream, startOffset, size);
} finally {
StreamUtil.close(origStream);
StreamUtil.flushAndCloseStream(destStream);
@@ -1023,6 +1041,19 @@
}
/**
+ * Helper method to calculate CRC-32 for a file.
+ *
+ * @param file
+ * @return CRC-32 of the file
+ * @throws IOException
+ */
+ public static long calculateCrc32(File file) throws IOException {
+ try (BufferedInputStream inputSource = new BufferedInputStream(new FileInputStream(file))) {
+ return StreamUtil.calculateCrc32(inputSource);
+ }
+ }
+
+ /**
* Helper method to calculate md5 for a file.
*
* @param file
diff --git a/src/com/android/tradefed/util/IEmail.java b/common_util/com/android/tradefed/util/IEmail.java
similarity index 100%
rename from src/com/android/tradefed/util/IEmail.java
rename to common_util/com/android/tradefed/util/IEmail.java
diff --git a/src/com/android/tradefed/util/IRunUtil.java b/common_util/com/android/tradefed/util/IRunUtil.java
similarity index 99%
rename from src/com/android/tradefed/util/IRunUtil.java
rename to common_util/com/android/tradefed/util/IRunUtil.java
index 3a36b7d..6b73d8d 100644
--- a/src/com/android/tradefed/util/IRunUtil.java
+++ b/common_util/com/android/tradefed/util/IRunUtil.java
@@ -17,7 +17,6 @@
package com.android.tradefed.util;
import com.android.annotations.Nullable;
-import com.android.tradefed.command.CommandScheduler;
import java.io.File;
import java.io.IOException;
diff --git a/src/com/android/tradefed/util/MultiMap.java b/common_util/com/android/tradefed/util/MultiMap.java
similarity index 97%
rename from src/com/android/tradefed/util/MultiMap.java
rename to common_util/com/android/tradefed/util/MultiMap.java
index b592440..51c2f64 100644
--- a/src/com/android/tradefed/util/MultiMap.java
+++ b/common_util/com/android/tradefed/util/MultiMap.java
@@ -48,6 +48,13 @@
}
}
+ public MultiMap(Map<K, V> map) {
+ this();
+ for (K key : map.keySet()) {
+ put(key, map.get(key));
+ }
+ }
+
/**
* Clears the map.
*/
diff --git a/src/com/android/tradefed/util/RunInterruptedException.java b/common_util/com/android/tradefed/util/RunInterruptedException.java
similarity index 100%
rename from src/com/android/tradefed/util/RunInterruptedException.java
rename to common_util/com/android/tradefed/util/RunInterruptedException.java
diff --git a/src/com/android/tradefed/util/RunUtil.java b/common_util/com/android/tradefed/util/RunUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/RunUtil.java
rename to common_util/com/android/tradefed/util/RunUtil.java
diff --git a/src/com/android/tradefed/util/StreamUtil.java b/common_util/com/android/tradefed/util/StreamUtil.java
similarity index 78%
rename from src/com/android/tradefed/util/StreamUtil.java
rename to common_util/com/android/tradefed/util/StreamUtil.java
index 2f1d009..f6fce21 100644
--- a/src/com/android/tradefed/util/StreamUtil.java
+++ b/common_util/com/android/tradefed/util/StreamUtil.java
@@ -23,6 +23,8 @@
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -35,6 +37,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Objects;
+import java.util.zip.CRC32;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipOutputStream;
@@ -170,16 +173,51 @@
*
* @param inStream the {@link InputStream}
* @param outStream the {@link OutputStream}
- * @param offset The offset of when to start copying the data.
+ * @param offset the offset of when to start copying the data.
* @throws IOException
*/
public static void copyStreams(InputStream inStream, OutputStream outStream, int offset)
throws IOException {
+ // Set size to a negative value to copy all content starting at the given offset.
+ copyStreams(inStream, outStream, offset, -1);
+ }
+
+ /**
+ * Copies contents of origStream to destStream starting at a given offset with a specific size.
+ *
+ * <p>Recommended to provide a buffered stream for input and output
+ *
+ * @param inStream the {@link InputStream}
+ * @param outStream the {@link OutputStream}
+ * @param offset the offset of when to start copying the data.
+ * @param size the number of bytes to copy. A negative value means to copy all content.
+ * @throws IOException
+ */
+ public static void copyStreams(
+ InputStream inStream, OutputStream outStream, long offset, long size)
+ throws IOException {
+ assert offset >= 0 : "offset must be greater or equal to zero.";
+ assert size != 0 : "size cannot be zero.";
inStream.skip(offset);
byte[] buf = new byte[BUF_SIZE];
- int size = -1;
- while ((size = inStream.read(buf)) != -1) {
- outStream.write(buf, 0, size);
+ long totalRetrievedSize = 0;
+ int retrievedSize = -1;
+ while ((retrievedSize = inStream.read(buf)) != -1) {
+ if (size > 0 && size < totalRetrievedSize + retrievedSize) {
+ retrievedSize = (int) (size - totalRetrievedSize);
+ }
+ outStream.write(buf, 0, retrievedSize);
+ totalRetrievedSize += retrievedSize;
+ if (size == totalRetrievedSize) {
+ break;
+ }
+ }
+ if (size > 0 && size > totalRetrievedSize) {
+ throw new IOException(
+ String.format(
+ "Failed to read %d bytes starting at offset %d, only %d bytes "
+ + "retrieved.",
+ size, offset, totalRetrievedSize));
}
}
@@ -201,6 +239,24 @@
}
/**
+ * Copies contents of file to outStream. It is recommended to provide a buffered stream.
+ *
+ * @param file the {@link File}
+ * @param outStream the {@link OutputStream}
+ * @throws IOException
+ */
+ public static void copyFileToStream(File file, OutputStream outStream) throws IOException {
+ InputStream inStream = null;
+ try {
+ inStream = new FileInputStream(file);
+ inStream = new BufferedInputStream(inStream);
+ StreamUtil.copyStreams(inStream, outStream);
+ } finally {
+ StreamUtil.close(inStream);
+ }
+ }
+
+ /**
* Gets the stack trace as a {@link String}.
*
* @param throwable the {@link Throwable} to convert.
@@ -314,6 +370,28 @@
}
/**
+ * Helper method to calculate CRC-32 for an {@link InputStream}. The stream will be consumed and
+ * closed. It is recommended to provide a buffered stream.
+ *
+ * @param inStream the {@link InputStream}
+ * @return CRC-32 of the stream
+ * @throws IOException
+ */
+ public static long calculateCrc32(InputStream inStream) throws IOException {
+ CRC32 crc32 = new CRC32();
+ byte[] buf = new byte[BUF_SIZE];
+ int size = -1;
+ try {
+ while ((size = inStream.read(buf)) >= 0) {
+ crc32.update(buf, 0, size);
+ }
+ } finally {
+ inStream.close();
+ }
+ return crc32.getValue();
+ }
+
+ /**
* Helper method to calculate md5 for a inputStream. The inputStream will be consumed and
* closed.
*
diff --git a/src/com/android/tradefed/util/TimeUtil.java b/common_util/com/android/tradefed/util/TimeUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/TimeUtil.java
rename to common_util/com/android/tradefed/util/TimeUtil.java
diff --git a/src/com/android/tradefed/util/UniqueMultiMap.java b/common_util/com/android/tradefed/util/UniqueMultiMap.java
similarity index 100%
rename from src/com/android/tradefed/util/UniqueMultiMap.java
rename to common_util/com/android/tradefed/util/UniqueMultiMap.java
diff --git a/src/com/android/tradefed/util/VersionParser.java b/common_util/com/android/tradefed/util/VersionParser.java
similarity index 100%
rename from src/com/android/tradefed/util/VersionParser.java
rename to common_util/com/android/tradefed/util/VersionParser.java
diff --git a/common_util/com/android/tradefed/util/ZipUtil.java b/common_util/com/android/tradefed/util/ZipUtil.java
new file mode 100644
index 0000000..6204b8a
--- /dev/null
+++ b/common_util/com/android/tradefed/util/ZipUtil.java
@@ -0,0 +1,575 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.zip.CentralDirectoryInfo;
+import com.android.tradefed.util.zip.EndCentralDirectoryInfo;
+import com.android.tradefed.util.zip.LocalFileHeader;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.zip.DataFormatException;
+import java.util.zip.GZIPOutputStream;
+import java.util.zip.Inflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * A helper class for compression-related operations
+ */
+public class ZipUtil {
+
+ private static final int COMPRESSION_METHOD_STORED = 0;
+ private static final int COMPRESSION_METHOD_DEFLATE = 8;
+ private static final String DEFAULT_DIRNAME = "dir";
+ private static final String DEFAULT_FILENAME = "files";
+ private static final String ZIP_EXTENSION = ".zip";
+ private static final String PARTIAL_ZIP_DATA = "compressed_data";
+
+ private static final boolean IS_UNIX;
+
+ static {
+ String OS = System.getProperty("os.name").toLowerCase();
+ IS_UNIX = (OS.contains("nix") || OS.contains("nux") || OS.contains("aix"));
+ }
+
+ /**
+ * Utility method to verify that a zip file is not corrupt.
+ *
+ * @param zipFile the {@link File} to check
+ * @param thorough Whether to attempt to fully extract the archive. If {@code false}, this
+ * method will fail to detect CRC errors in a well-formed archive.
+ * @throws IOException if the file could not be opened or read
+ * @return {@code false} if the file appears to be corrupt; {@code true} otherwise
+ */
+ public static boolean isZipFileValid(File zipFile, boolean thorough) throws IOException {
+ if (zipFile != null && !zipFile.exists()) {
+ CLog.d("Zip file does not exist: %s", zipFile.getAbsolutePath());
+ return false;
+ }
+
+ try (ZipFile z = new ZipFile(zipFile)) {
+ if (thorough) {
+ // Reading the entire file is the only way to detect CRC errors within the archive
+ final File extractDir = FileUtil.createTempDir("extract-" + zipFile.getName());
+ try {
+ extractZip(z, extractDir);
+ } finally {
+ FileUtil.recursiveDelete(extractDir);
+ }
+ }
+ } catch (ZipException e) {
+ // File is likely corrupted
+ CLog.d("Detected corrupt zip file %s:", zipFile.getCanonicalPath());
+ CLog.e(e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Utility method to extract entire contents of zip file into given directory
+ *
+ * @param zipFile the {@link ZipFile} to extract
+ * @param destDir the local dir to extract file to
+ * @throws IOException if failed to extract file
+ */
+ public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
+ Enumeration<? extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+
+ ZipEntry entry = entries.nextElement();
+ File childFile = new File(destDir, entry.getName());
+ childFile.getParentFile().mkdirs();
+ if (entry.isDirectory()) {
+ continue;
+ } else {
+ FileUtil.writeToFile(zipFile.getInputStream(entry), childFile);
+ }
+ }
+ }
+
+ /**
+ * Utility method to extract one specific file from zip file into a tmp file
+ *
+ * @param zipFile the {@link ZipFile} to extract
+ * @param filePath the filePath of to extract
+ * @throws IOException if failed to extract file
+ * @return the {@link File} or null if not found
+ */
+ public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
+ ZipEntry entry = zipFile.getEntry(filePath);
+ if (entry == null) {
+ return null;
+ }
+ File createdFile = FileUtil.createTempFile("extracted",
+ FileUtil.getExtension(filePath));
+ FileUtil.writeToFile(zipFile.getInputStream(entry), createdFile);
+ return createdFile;
+ }
+
+ /**
+ * Utility method to create a temporary zip file containing the given directory and
+ * all its contents.
+ *
+ * @param dir the directory to zip
+ * @return a temporary zip {@link File} containing directory contents
+ * @throws IOException if failed to create zip file
+ */
+ public static File createZip(File dir) throws IOException {
+ return createZip(dir, DEFAULT_DIRNAME);
+ }
+
+ /**
+ * Utility method to create a temporary zip file containing the given directory and
+ * all its contents.
+ *
+ * @param dir the directory to zip
+ * @param name the base name of the zip file created without the extension.
+ * @return a temporary zip {@link File} containing directory contents
+ * @throws IOException if failed to create zip file
+ */
+ public static File createZip(File dir, String name) throws IOException {
+ File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
+ createZip(dir, zipFile);
+ return zipFile;
+ }
+
+ /**
+ * Utility method to create a zip file containing the given directory and
+ * all its contents.
+ *
+ * @param dir the directory to zip
+ * @param zipFile the zip file to create - it should not already exist
+ * @throws IOException if failed to create zip file
+ */
+ public static void createZip(File dir, File zipFile) throws IOException {
+ ZipOutputStream out = null;
+ try {
+ FileOutputStream fileStream = new FileOutputStream(zipFile);
+ out = new ZipOutputStream(new BufferedOutputStream(fileStream));
+ addToZip(out, dir, new LinkedList<String>());
+ } catch (IOException e) {
+ zipFile.delete();
+ throw e;
+ } catch (RuntimeException e) {
+ zipFile.delete();
+ throw e;
+ } finally {
+ StreamUtil.close(out);
+ }
+ }
+
+ /**
+ * Utility method to create a temporary zip file containing the given files
+ *
+ * @param files list of files to zip
+ * @return a temporary zip {@link File} containing directory contents
+ * @throws IOException if failed to create zip file
+ */
+ public static File createZip(List<File> files) throws IOException {
+ return createZip(files, DEFAULT_FILENAME);
+ }
+
+ /**
+ * Utility method to create a temporary zip file containing the given files.
+ *
+ * @param files list of files to zip
+ * @param name the base name of the zip file created without the extension.
+ * @return a temporary zip {@link File} containing directory contents
+ * @throws IOException if failed to create zip file
+ */
+ public static File createZip(List<File> files, String name) throws IOException {
+ File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
+ createZip(files, zipFile);
+ return zipFile;
+ }
+
+ /**
+ * Utility method to create a zip file containing the given files
+ *
+ * @param files list of files to zip
+ * @param zipFile the zip file to create - it should not already exist
+ * @throws IOException if failed to create zip file
+ */
+ public static void createZip(List<File> files, File zipFile) throws IOException {
+ ZipOutputStream out = null;
+ try {
+ FileOutputStream fileStream = new FileOutputStream(zipFile);
+ out = new ZipOutputStream(new BufferedOutputStream(fileStream));
+ for (File file : files) {
+ addToZip(out, file, new LinkedList<String>());
+ }
+ } catch (IOException|RuntimeException e) {
+ zipFile.delete();
+ throw e;
+ } finally {
+ StreamUtil.close(out);
+ }
+ }
+
+ /**
+ * Recursively adds given file and its contents to ZipOutputStream
+ *
+ * @param out the {@link ZipOutputStream}
+ * @param file the {@link File} to add to the stream
+ * @param relativePathSegs the relative path of file, including separators
+ * @throws IOException if failed to add file to zip
+ */
+ public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
+ throws IOException {
+ relativePathSegs.add(file.getName());
+ if (file.isDirectory()) {
+ // note: it appears even on windows, ZipEntry expects '/' as a path separator
+ relativePathSegs.add("/");
+ }
+ ZipEntry zipEntry = new ZipEntry(buildPath(relativePathSegs));
+ out.putNextEntry(zipEntry);
+ if (file.isFile()) {
+ writeToStream(file, out);
+ }
+ out.closeEntry();
+ if (file.isDirectory()) {
+ // recursively add contents
+ File[] subFiles = file.listFiles();
+ if (subFiles == null) {
+ throw new IOException(String.format("Could not read directory %s",
+ file.getAbsolutePath()));
+ }
+ for (File subFile : subFiles) {
+ addToZip(out, subFile, relativePathSegs);
+ }
+ // remove the path separator
+ relativePathSegs.remove(relativePathSegs.size()-1);
+ }
+ // remove the last segment, added at beginning of method
+ relativePathSegs.remove(relativePathSegs.size()-1);
+ }
+
+ /**
+ * Close an open {@link ZipFile}, ignoring any exceptions.
+ *
+ * @param zipFile the file to close
+ */
+ public static void closeZip(ZipFile zipFile) {
+ if (zipFile != null) {
+ try {
+ zipFile.close();
+ } catch (IOException e) {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Helper method to create a gzipped version of a single file.
+ *
+ * @param file the original file
+ * @param gzipFile the file to place compressed contents in
+ * @throws IOException
+ */
+ public static void gzipFile(File file, File gzipFile) throws IOException {
+ GZIPOutputStream out = null;
+ try {
+ FileOutputStream fileStream = new FileOutputStream(gzipFile);
+ out = new GZIPOutputStream(new BufferedOutputStream(fileStream, 64 * 1024));
+ writeToStream(file, out);
+ } catch (IOException e) {
+ gzipFile.delete();
+ throw e;
+ } catch (RuntimeException e) {
+ gzipFile.delete();
+ throw e;
+ } finally {
+ StreamUtil.close(out);
+ }
+ }
+
+ /**
+ * Helper method to write input file contents to output stream.
+ *
+ * @param file the input {@link File}
+ * @param out the {@link OutputStream}
+ *
+ * @throws IOException
+ */
+ private static void writeToStream(File file, OutputStream out) throws IOException {
+ InputStream inputStream = null;
+ try {
+ inputStream = new BufferedInputStream(new FileInputStream(file));
+ StreamUtil.copyStreams(inputStream, out);
+ } finally {
+ StreamUtil.close(inputStream);
+ }
+ }
+
+ /**
+ * Builds a file system path from a stack of relative path segments
+ *
+ * @param relativePathSegs the list of relative paths
+ * @return a {@link String} containing all relativePathSegs
+ */
+ private static String buildPath(List<String> relativePathSegs) {
+ StringBuilder pathBuilder = new StringBuilder();
+ for (String segment : relativePathSegs) {
+ pathBuilder.append(segment);
+ }
+ return pathBuilder.toString();
+ }
+
+ /**
+ * Extract a zip file to a temp directory prepended with a string
+ *
+ * @param zipFile the zip file to extract
+ * @param nameHint a prefix for the temp directory
+ * @return a {@link File} pointing to the temp directory
+ */
+ public static File extractZipToTemp(File zipFile, String nameHint)
+ throws IOException, ZipException {
+ File localRootDir = FileUtil.createTempDir(nameHint);
+ try (ZipFile zip = new ZipFile(zipFile)) {
+ extractZip(zip, localRootDir);
+ return localRootDir;
+ } catch (IOException e) {
+ // clean tmp file since we couldn't extract.
+ FileUtil.recursiveDelete(localRootDir);
+ throw e;
+ }
+ }
+
+ /**
+ * Get a list of {link CentralDirectoryInfo} for files in a zip file.
+ *
+ * @param partialZipFile a {@link File} object of the partial zip file that contains central
+ * directory entries.
+ * @param endCentralDirInfo a {@link EndCentralDirectoryInfo} object of the zip file.
+ * @return A list of {@link CentralDirectoryInfo} of the zip file
+ * @throws IOException
+ */
+ public static List<CentralDirectoryInfo> getZipCentralDirectoryInfos(
+ File partialZipFile, EndCentralDirectoryInfo endCentralDirInfo) throws IOException {
+ return getZipCentralDirectoryInfos(partialZipFile, endCentralDirInfo, 0);
+ }
+
+ /**
+ * Get a list of {link CentralDirectoryInfo} for files in a zip file.
+ *
+ * @param partialZipFile a {@link File} object of the partial zip file that contains central
+ * directory entries.
+ * @param endCentralDirInfo a {@link EndCentralDirectoryInfo} object of the zip file.
+ * @param offset the offset in the partial zip file where the content of central directory
+ * entries starts.
+ * @return A list of {@link CentralDirectoryInfo} of the zip file
+ * @throws IOException
+ */
+ public static List<CentralDirectoryInfo> getZipCentralDirectoryInfos(
+ File partialZipFile, EndCentralDirectoryInfo endCentralDirInfo, long offset)
+ throws IOException {
+ List<CentralDirectoryInfo> infos = new ArrayList<>();
+ byte[] data;
+ try (FileInputStream stream = new FileInputStream(partialZipFile)) {
+ // Read in the entire central directory block for a zip file till the end. The block
+ // should be small even for a large zip file.
+ long totalSize = stream.getChannel().size();
+ stream.skip(offset);
+ data = new byte[(int) (totalSize - offset)];
+ stream.read(data);
+ }
+ int startOffset = 0;
+ for (int i = 0; i < endCentralDirInfo.getEntryNumber(); i++) {
+ CentralDirectoryInfo info = new CentralDirectoryInfo(data, startOffset);
+ infos.add(info);
+ startOffset += info.getInfoSize();
+ }
+
+ return infos;
+ }
+
+ /**
+ * Apply the file permission configured in the central directory entry.
+ *
+ * @param targetFile the {@link File} to set permission to.
+ * @param zipEntry a {@link CentralDirectoryInfo} object that contains the file permissions.
+ * @throws IOException if fail to access the file.
+ */
+ public static void applyPermission(File targetFile, CentralDirectoryInfo zipEntry)
+ throws IOException {
+ if (!IS_UNIX) {
+ CLog.w("Permission setting is only supported in Unix/Linux system.");
+ return;
+ }
+
+ if (zipEntry.getFilePermission() != 0) {
+ Files.setPosixFilePermissions(
+ targetFile.toPath(), FileUtil.unixModeToPosix(zipEntry.getFilePermission()));
+ }
+ }
+
+ /**
+ * Extract the requested folder from a partial zip file and apply proper permission.
+ *
+ * @param targetFile the {@link File} to save the extracted file to.
+ * @param zipEntry a {@link CentralDirectoryInfo} object of the file to extract from the partial
+ * zip file.
+ * @throws IOException
+ */
+ public static void unzipPartialZipFolder(File targetFile, CentralDirectoryInfo zipEntry)
+ throws IOException {
+ unzipPartialZipFile(null, targetFile, zipEntry, null, -1);
+ }
+
+ /**
+ * Extract the requested file from a partial zip file.
+ *
+ * <p>This method assumes all files are on the same disk when compressed. It doesn't support
+ * following features yet:
+ *
+ * <p>Zip file larger than 4GB
+ *
+ * <p>ZIP64(require ZipLocalFileHeader update on compressed size)
+ *
+ * <p>Encrypted zip file
+ *
+ * <p>Symlink
+ *
+ * @param partialZip a {@link File} that's a partial of the zip file.
+ * @param targetFile the {@link File} to save the extracted file to.
+ * @param zipEntry a {@link CentralDirectoryInfo} object of the file to extract from the partial
+ * zip file.
+ * @param localFileHeader a {@link LocalFileHeader} object of the file to extract from the
+ * partial zip file.
+ * @param startOffset start offset of the file to extract.
+ * @throws IOException
+ */
+ public static void unzipPartialZipFile(
+ File partialZip,
+ File targetFile,
+ CentralDirectoryInfo zipEntry,
+ LocalFileHeader localFileHeader,
+ long startOffset)
+ throws IOException {
+ try {
+ if (zipEntry.getFileName().endsWith("/")) {
+ // Create a folder.
+ targetFile.mkdir();
+ return;
+ } else if (zipEntry.getCompressedSize() == 0) {
+ // The file is empty, just create an empty file.
+ targetFile.createNewFile();
+ return;
+ }
+
+ File zipFile = targetFile;
+ if (zipEntry.getCompressionMethod() != COMPRESSION_METHOD_STORED)
+ // Create a temp file to store the compressed data, then unzip it.
+ zipFile = FileUtil.createTempFile(PARTIAL_ZIP_DATA, ZIP_EXTENSION);
+ else {
+ // The file is not compressed, stream it directly to the target.
+ zipFile.getParentFile().mkdirs();
+ zipFile.createNewFile();
+ }
+
+ // Save compressed data to zipFile
+ try (FileInputStream stream = new FileInputStream(partialZip)) {
+ FileUtil.writeToFile(
+ stream,
+ zipFile,
+ false,
+ startOffset + localFileHeader.getHeaderSize(),
+ zipEntry.getCompressedSize());
+ }
+
+ if (zipEntry.getCompressionMethod() == COMPRESSION_METHOD_STORED) {
+ return;
+ } else if (zipEntry.getCompressionMethod() == COMPRESSION_METHOD_DEFLATE) {
+ boolean success = false;
+ try {
+ unzipRawZip(zipFile, targetFile, zipEntry);
+ success = true;
+ } catch (DataFormatException e) {
+ throw new IOException(e);
+ } finally {
+ zipFile.delete();
+ if (!success) {
+ CLog.e("Failed to unzip %s", zipEntry.getFileName());
+ targetFile.delete();
+ }
+ }
+ } else {
+ throw new RuntimeException(
+ String.format(
+ "Compression method %d is not supported.",
+ localFileHeader.getCompressionMethod()));
+ }
+ } finally {
+ if (targetFile.exists()) {
+ applyPermission(targetFile, zipEntry);
+ }
+ }
+ }
+
+ /**
+ * Unzip the raw compressed content without wrapper (local file header).
+ *
+ * @param zipFile the {@link File} that contains the compressed data of the target file.
+ * @param targetFile {@link File} to same the decompressed data to.
+ * @throws DataFormatException if decompression failed due to zip format issue.
+ * @throws IOException if failed to access the compressed data or the decompressed file has
+ * mismatched CRC.
+ */
+ private static void unzipRawZip(File zipFile, File targetFile, CentralDirectoryInfo zipEntry)
+ throws IOException, DataFormatException {
+ Inflater decompresser = new Inflater(true);
+
+ targetFile.getParentFile().mkdirs();
+ targetFile.createNewFile();
+
+ try (FileInputStream inputStream = new FileInputStream(zipFile);
+ FileOutputStream outputStream = new FileOutputStream(targetFile)) {
+ byte[] data = new byte[32768];
+ byte[] buffer = new byte[65536];
+ while (inputStream.read(data) > 0) {
+ decompresser.setInput(data);
+ while (!decompresser.finished() && !decompresser.needsInput()) {
+ int size = decompresser.inflate(buffer);
+ outputStream.write(buffer, 0, size);
+ }
+ }
+ } finally {
+ decompresser.end();
+ }
+
+ // Validate CRC
+ if (FileUtil.calculateCrc32(targetFile) != zipEntry.getCrc()) {
+ throw new IOException(String.format("Failed to match CRC for file %s", targetFile));
+ }
+ }
+}
diff --git a/src/com/android/tradefed/util/ZipUtil2.java b/common_util/com/android/tradefed/util/ZipUtil2.java
similarity index 100%
rename from src/com/android/tradefed/util/ZipUtil2.java
rename to common_util/com/android/tradefed/util/ZipUtil2.java
diff --git a/src/com/android/tradefed/util/net/HttpHelper.java b/common_util/com/android/tradefed/util/net/HttpHelper.java
similarity index 100%
rename from src/com/android/tradefed/util/net/HttpHelper.java
rename to common_util/com/android/tradefed/util/net/HttpHelper.java
diff --git a/src/com/android/tradefed/util/net/HttpMultipartPost.java b/common_util/com/android/tradefed/util/net/HttpMultipartPost.java
similarity index 100%
rename from src/com/android/tradefed/util/net/HttpMultipartPost.java
rename to common_util/com/android/tradefed/util/net/HttpMultipartPost.java
diff --git a/src/com/android/tradefed/util/net/IHttpHelper.java b/common_util/com/android/tradefed/util/net/IHttpHelper.java
similarity index 100%
rename from src/com/android/tradefed/util/net/IHttpHelper.java
rename to common_util/com/android/tradefed/util/net/IHttpHelper.java
diff --git a/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
new file mode 100644
index 0000000..49ec5a7
--- /dev/null
+++ b/common_util/com/android/tradefed/util/zip/CentralDirectoryInfo.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util.zip;
+
+import com.android.tradefed.util.ByteArrayUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * CentralDirectoryInfo is a class containing the information of a file/folder inside a zip file.
+ *
+ * <p>Overall zipfile format: [Local file header + Compressed data [+ Extended local header]?]*
+ * [Central directory]* [End of central directory record]
+ *
+ * <p>Refer to following link for more details: https://en.wikipedia.org/wiki/Zip_(file_format)
+ */
+public final class CentralDirectoryInfo {
+
+ private static final byte[] CENTRAL_DIRECTORY_SIGNATURE = {0x50, 0x4b, 0x01, 0x02};
+
+ private int mCompressionMethod;
+ private long mCrc;
+ private long mCompressedSize;
+ private long mUncompressedSize;
+ private long mLocalHeaderOffset;
+ private int mInternalFileAttributes;
+ private long mExternalFileAttributes;
+ private String mFileName;
+ private int mFileNameLength;
+ private int mExtraFieldLength;
+ private int mFileCommentLength;
+
+ /** Get the compression method. */
+ public int getCompressionMethod() {
+ return mCompressionMethod;
+ }
+
+ /** Set the compression method. */
+ public void setCompressionMethod(int compressionMethod) {
+ mCompressionMethod = compressionMethod;
+ }
+
+ /** Get the CRC of the file. */
+ public long getCrc() {
+ return mCrc;
+ }
+
+ /** Set the CRC of the file. */
+ public void setCrc(long crc) {
+ mCrc = crc;
+ }
+
+ /** Get the compressed size. */
+ public int getCompressedSize() {
+ return (int) mCompressedSize;
+ }
+
+ /** Set the compressed size. */
+ public void setCompressedSize(long compressionSize) {
+ mCompressedSize = compressionSize;
+ }
+
+ /** Get the uncompressed size. */
+ public long getUncompressedSize() {
+ return mUncompressedSize;
+ }
+
+ /** Set the uncompressed size. */
+ public void setUncompressedSize(long uncompressedSize) {
+ mUncompressedSize = uncompressedSize;
+ }
+
+ /** Get the offset of local file header entry. */
+ public long getLocalHeaderOffset() {
+ return mLocalHeaderOffset;
+ }
+
+ /** Set the offset of local file header entry. */
+ public void setLocalHeaderOffset(long localHeaderOffset) {
+ mLocalHeaderOffset = localHeaderOffset;
+ }
+
+ /** Get the internal file attributes. */
+ public int getInternalFileAttributes() {
+ return mInternalFileAttributes;
+ }
+
+ /** Set the internal file attributes. */
+ public void setInternalFileAttributes(int internalFileAttributes) {
+ mInternalFileAttributes = internalFileAttributes;
+ }
+
+ /** Get the external file attributes. */
+ public long getExternalFileAttributes() {
+ return mExternalFileAttributes;
+ }
+
+ /** Set the external file attributes. */
+ public void setExternalFileAttributes(long externalFileAttributes) {
+ mExternalFileAttributes = externalFileAttributes;
+ }
+
+ /** Get the Linux file permission, stored in the last 9 bits of external file attributes. */
+ public int getFilePermission() {
+ return ((int) mExternalFileAttributes & (0777 << 16L)) >> 16L;
+ }
+
+ /** Get the file name including the relative path. */
+ public String getFileName() {
+ return mFileName;
+ }
+
+ /** Set the file name including the relative path. */
+ public void setFileName(String fileName) {
+ mFileName = fileName;
+ }
+
+ /** Get the file name length. */
+ public int getFileNameLength() {
+ return mFileNameLength;
+ }
+
+ /** Set the file name length. */
+ public void setFileNameLength(int fileNameLength) {
+ mFileNameLength = fileNameLength;
+ }
+
+ /** Get the extra field length. */
+ public int getExtraFieldLength() {
+ return mExtraFieldLength;
+ }
+
+ /** Set the extra field length. */
+ public void setExtraFieldLength(int extraFieldLength) {
+ mExtraFieldLength = extraFieldLength;
+ }
+
+ /** Get the file comment length. */
+ public int getFileCommentLength() {
+ return mFileCommentLength;
+ }
+
+ /** Set the file comment length. */
+ public void setFileCommentLength(int fileCommentLength) {
+ mFileCommentLength = fileCommentLength;
+ }
+
+ /** Get the size of the central directory entry. */
+ public int getInfoSize() {
+ return 46 + mFileNameLength + mExtraFieldLength + mFileCommentLength;
+ }
+
+ /** Default constructor used for unit test. */
+ @VisibleForTesting
+ protected CentralDirectoryInfo() {}
+
+ /**
+ * Constructor to collect the information of a file entry inside zip file.
+ *
+ * @param data {@code byte[]} of data that contains the information of a file entry.
+ * @param startOffset start offset of the information block.
+ * @throws IOException
+ */
+ public CentralDirectoryInfo(byte[] data, int startOffset) throws IOException {
+ // Central directory:
+ // Offset Length Contents
+ // 0 4 bytes Central file header signature (0x02014b50)
+ // 4 2 bytes Version made by
+ // 6 2 bytes Version needed to extract
+ // 8 2 bytes General purpose bit flag
+ // 10 2 bytes Compression method
+ // 12 2 bytes Last mod file time
+ // 14 2 bytes Last mod file date
+ // 16 4 bytes CRC-32
+ // 20 4 bytes Compressed size
+ // 24 4 bytes Uncompressed size
+ // 28 2 bytes Filename length (f)
+ // 30 2 bytes Extra field length (e)
+ // 32 2 bytes File comment length (c)
+ // 34 2 bytes Disk number start
+ // 36 2 bytes Internal file attributes
+ // 38 4 bytes External file attributes (file permission stored in the last 9 bits)
+ // 42 4 bytes Relative offset of local header
+ // 46 (f)bytes Filename
+ // (e)bytes Extra field
+ // (c)bytes File comment
+
+ // Check signature
+ if (!Arrays.equals(
+ CENTRAL_DIRECTORY_SIGNATURE,
+ Arrays.copyOfRange(data, startOffset, startOffset + 4))) {
+ throw new IOException("Invalid central directory info for zip file is found.");
+ }
+ mCompressionMethod = ByteArrayUtil.getInt(data, startOffset + 10, 2);
+ mCrc = ByteArrayUtil.getLong(data, startOffset + 16, 4);
+ mCompressedSize = ByteArrayUtil.getLong(data, startOffset + 20, 4);
+ mUncompressedSize = ByteArrayUtil.getLong(data, startOffset + 24, 4);
+ mInternalFileAttributes = ByteArrayUtil.getInt(data, startOffset + 36, 2);
+ mExternalFileAttributes = ByteArrayUtil.getLong(data, startOffset + 38, 4);
+ mLocalHeaderOffset = ByteArrayUtil.getLong(data, startOffset + 42, 4);
+ mFileNameLength = ByteArrayUtil.getInt(data, startOffset + 28, 2);
+ mFileName = ByteArrayUtil.getString(data, startOffset + 46, mFileNameLength);
+ mExtraFieldLength = ByteArrayUtil.getInt(data, startOffset + 30, 2);
+ mFileCommentLength = ByteArrayUtil.getInt(data, startOffset + 32, 2);
+ }
+}
diff --git a/common_util/com/android/tradefed/util/zip/EndCentralDirectoryInfo.java b/common_util/com/android/tradefed/util/zip/EndCentralDirectoryInfo.java
new file mode 100644
index 0000000..b675ebb
--- /dev/null
+++ b/common_util/com/android/tradefed/util/zip/EndCentralDirectoryInfo.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util.zip;
+
+import com.android.tradefed.util.ByteArrayUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * EndCentralDirectoryInfo is a class containing the overall information of a zip file. It's at the
+ * end of the zip file.
+ *
+ * <p>Overall zipfile format: [Local file header + Compressed data [+ Extended local header]?]*
+ * [Central directory]* [End of central directory record]
+ *
+ * <p>Refer to following link for more details: https://en.wikipedia.org/wiki/Zip_(file_format)
+ */
+public final class EndCentralDirectoryInfo {
+
+ // End central directory of a zip file is at the end of the file, and its size shouldn't be
+ // larger than 64k.
+ public static final int MAX_LOOKBACK = 64 * 1024;
+
+ private static final byte[] END_CENTRAL_DIRECTORY_SIGNATURE = {0x50, 0x4b, 0x05, 0x06};
+ // Central directory signature is always 4 bytes.
+ private static final int CENTRAL_DIRECTORY_MAGIC_LENGTH = 4;
+
+ private int mEntryNumber;
+ private long mCentralDirSize;
+ private long mCentralDirOffset;
+
+ public int getEntryNumber() {
+ return mEntryNumber;
+ }
+
+ public long getCentralDirSize() {
+ return mCentralDirSize;
+ }
+
+ public long getCentralDirOffset() {
+ return mCentralDirOffset;
+ }
+
+ /**
+ * Constructor to collect end central directory information of a zip file.
+ *
+ * @param zipFile a {@link File} contains the end central directory information. It's likely the
+ * ending part of the zip file.
+ * @throws IOException
+ */
+ public EndCentralDirectoryInfo(File zipFile) throws IOException {
+ // End of central directory record:
+ // Offset Length Contents
+ // 0 4 bytes End of central dir signature (0x06054b50)
+ // 4 2 bytes Number of this disk
+ // 6 2 bytes Number of the disk with the start of the central directory
+ // 8 2 bytes Total number of entries in the central dir on this disk
+ // 10 2 bytes Total number of entries in the central dir
+ // 12 4 bytes Size of the central directory
+ // 16 4 bytes Offset of start of central directory with respect to the starting
+ // disk number
+ // 20 2 bytes zipfile comment length (c)
+ // 22 (c)bytes zipfile comment
+
+ try (FileInputStream stream = new FileInputStream(zipFile)) {
+ long size = stream.getChannel().size();
+ if (size > MAX_LOOKBACK) {
+ stream.skip(size - MAX_LOOKBACK);
+ size = MAX_LOOKBACK;
+ }
+ byte[] endCentralDir = new byte[(int) size];
+ stream.read(endCentralDir);
+ int offset = (int) size - CENTRAL_DIRECTORY_MAGIC_LENGTH - 1;
+ // Seek from the end of the file, searching for the end central directory signature.
+ while (offset >= 0) {
+ if (!java.util.Arrays.equals(
+ END_CENTRAL_DIRECTORY_SIGNATURE,
+ Arrays.copyOfRange(endCentralDir, offset, offset + 4))) {
+ offset--;
+ continue;
+ }
+ // Get the total number of entries in the central directory
+ mEntryNumber = ByteArrayUtil.getInt(endCentralDir, offset + 10, 2);
+ // Get the size of the central directory block
+ mCentralDirSize = ByteArrayUtil.getLong(endCentralDir, offset + 12, 4);
+ // Get the offset of start of central directory
+ mCentralDirOffset = ByteArrayUtil.getLong(endCentralDir, offset + 16, 4);
+
+ if (mCentralDirOffset < 0) {
+ throw new IOException(
+ "Failed to get offset of EndCentralDirectoryInfo. Partial unzip doesn't support zip files larger than 4GB.");
+ }
+ break;
+ }
+ if (offset < 0) {
+ throw new RuntimeException(
+ "Failed to find end central directory info for zip file: "
+ + zipFile.getPath());
+ }
+ }
+ }
+}
diff --git a/common_util/com/android/tradefed/util/zip/LocalFileHeader.java b/common_util/com/android/tradefed/util/zip/LocalFileHeader.java
new file mode 100644
index 0000000..2012189
--- /dev/null
+++ b/common_util/com/android/tradefed/util/zip/LocalFileHeader.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util.zip;
+
+import com.android.tradefed.util.ByteArrayUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * LocalFileHeader is a class containing the information of a file/folder inside a zip file. The
+ * block of data is at the beginning part of each file entry.
+ *
+ * <p>Overall zipfile format: [Local file header + Compressed data [+ Extended local header]?]*
+ * [Central directory]* [End of central directory record]
+ *
+ * <p>Refer to following link for more details: https://en.wikipedia.org/wiki/Zip_(file_format)
+ */
+public final class LocalFileHeader {
+
+ public static final int LOCAL_FILE_HEADER_SIZE = 30;
+ private static final byte[] LOCAL_FILE_HEADER_SIGNATURE = {0x50, 0x4b, 0x03, 0x04};
+
+ private int mCompressionMethod;
+ private long mCrc;
+ private long mCompressedSize;
+ private long mUncompressedSize;
+ private int mFileNameLength;
+ private int mExtraFieldLength;
+
+ public int getCompressionMethod() {
+ return mCompressionMethod;
+ }
+
+ public long getCrc() {
+ return mCrc;
+ }
+
+ public long getCompressedSize() {
+ return mCompressedSize;
+ }
+
+ public long getUncompressedSize() {
+ return mUncompressedSize;
+ }
+
+ public int getFileNameLength() {
+ return mFileNameLength;
+ }
+
+ public int getExtraFieldLength() {
+ return mExtraFieldLength;
+ }
+
+ public int getHeaderSize() {
+ return LOCAL_FILE_HEADER_SIZE + mFileNameLength + mExtraFieldLength;
+ }
+
+ public LocalFileHeader(File partialZipFile) throws IOException {
+ this(partialZipFile, 0);
+ }
+
+ /**
+ * Constructor to collect local file header information of a file entry in a zip file.
+ *
+ * @param partialZipFile a {@link File} contains the local file header information.
+ * @param startOffset the start offset of the block of data for a local file header.
+ * @throws IOException
+ */
+ public LocalFileHeader(File partialZipFile, int startOffset) throws IOException {
+ // Local file header:
+ // Offset Length Contents
+ // 0 4 bytes Local file header signature (0x04034b50)
+ // 4 2 bytes Version needed to extract
+ // 6 2 bytes General purpose bit flag
+ // 8 2 bytes Compression method
+ // 10 2 bytes Last mod file time
+ // 12 2 bytes Last mod file date
+ // 14 4 bytes CRC-32
+ // 18 4 bytes Compressed size (n)
+ // 22 4 bytes Uncompressed size
+ // 26 2 bytes Filename length (f)
+ // 28 2 bytes Extra field length (e)
+ // (f)bytes Filename
+ // (e)bytes Extra field
+ // (n)bytes Compressed data
+ byte[] data;
+ try (FileInputStream stream = new FileInputStream(partialZipFile)) {
+ stream.skip(startOffset);
+ data = new byte[LOCAL_FILE_HEADER_SIZE];
+ stream.read(data);
+ }
+
+ // Check signature
+ if (!Arrays.equals(LOCAL_FILE_HEADER_SIGNATURE, Arrays.copyOfRange(data, 0, 4))) {
+ throw new IOException("Invalid local file header for zip file is found.");
+ }
+ mCompressionMethod = ByteArrayUtil.getInt(data, 8, 2);
+ mCrc = ByteArrayUtil.getLong(data, 14, 4);
+ mCompressedSize = ByteArrayUtil.getLong(data, 18, 2);
+ mUncompressedSize = ByteArrayUtil.getLong(data, 22, 2);
+ mFileNameLength = ByteArrayUtil.getInt(data, 26, 2);
+ mExtraFieldLength = ByteArrayUtil.getInt(data, 28, 2);
+ }
+}
diff --git a/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java b/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java
new file mode 100644
index 0000000..ed1e8fc
--- /dev/null
+++ b/common_util/com/android/tradefed/util/zip/MergedZipEntryCollection.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util.zip;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Merge individual zip entries in a large zip file into blocks to minimize the download attempts.
+ */
+public class MergedZipEntryCollection {
+
+ // The maximum number of bytes between each section of merged zip entries.
+ public static final int MAX_GAP = 4096;
+
+ // The maximum percentage of gaps between zip entries over the total size of the download block.
+ public static final double MAX_GAP_PERCENTAGE = 0.15;
+
+ // Best guess of header size. 2k Should be more than enough for file path and extra attributes.
+ public static final int HEADER_SIZE = LocalFileHeader.LOCAL_FILE_HEADER_SIZE + 2048;
+
+ private List<CentralDirectoryInfo> mZipEntries;
+
+ public MergedZipEntryCollection(List<CentralDirectoryInfo> zipEntries) {
+ mZipEntries = zipEntries;
+ }
+
+ public long getStartOffset() {
+ return mZipEntries.get(0).getLocalHeaderOffset();
+ }
+
+ public long getEndOffset() {
+ CentralDirectoryInfo lastEntry = mZipEntries.get(mZipEntries.size() - 1);
+ return lastEntry.getLocalHeaderOffset() + lastEntry.getCompressedSize() + HEADER_SIZE;
+ }
+
+ public List<CentralDirectoryInfo> getZipEntries() {
+ return mZipEntries;
+ }
+
+ /*
+ * Merge a list of zip entries into groups to minimize download attempts.
+ *
+ * @param zipEntries a list of {@link CentralDirectoryInfo} for a zip file.
+ *
+ * @return a list of {@link MergedZipEntryCollection}, each contains a list of
+ * {@link CentralDirectoryInfo} that are stored closely inside the zip file.
+ */
+ public static List<MergedZipEntryCollection> CreateCollections(
+ List<CentralDirectoryInfo> zipEntries) {
+ if (zipEntries.size() == 0) {
+ return new ArrayList<MergedZipEntryCollection>();
+ }
+
+ // Sort the entries by start offset.
+ List<CentralDirectoryInfo> entries =
+ zipEntries
+ .stream()
+ .sorted(Comparator.comparing(CentralDirectoryInfo::getLocalHeaderOffset))
+ .collect(Collectors.toList());
+ long endOffset = -1;
+ long totalGap = 0;
+ List<MergedZipEntryCollection> collections = new ArrayList<>();
+ List<CentralDirectoryInfo> group = new ArrayList<>();
+ for (CentralDirectoryInfo entry : entries) {
+ if (endOffset >= 0) {
+ long newGap = entry.getLocalHeaderOffset() - endOffset + totalGap;
+ long totalSize =
+ entry.getLocalHeaderOffset()
+ + HEADER_SIZE
+ + entry.getCompressedSize()
+ - group.get(0).getLocalHeaderOffset();
+ double gapPercentage = (double) (newGap) / totalSize;
+ if (endOffset < entry.getLocalHeaderOffset() - MAX_GAP
+ && MAX_GAP_PERCENTAGE < gapPercentage) {
+ collections.add(new MergedZipEntryCollection(group));
+ group = new ArrayList<>();
+ totalGap = 0;
+ }
+ }
+ group.add(entry);
+ if (group.size() > 1 && entry.getLocalHeaderOffset() > endOffset) {
+ totalGap += entry.getLocalHeaderOffset() - endOffset;
+ }
+ endOffset = entry.getLocalHeaderOffset() + HEADER_SIZE + entry.getCompressedSize();
+ }
+ collections.add(new MergedZipEntryCollection(group));
+
+ return collections;
+ }
+}
diff --git a/device_build_interfaces/Android.bp b/device_build_interfaces/Android.bp
new file mode 100644
index 0000000..3d8a287
--- /dev/null
+++ b/device_build_interfaces/Android.bp
@@ -0,0 +1,32 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+ name: "tradefed-device-build-interfaces",
+ defaults: ["tradefed_defaults"],
+ srcs: [
+ "com/**/*.java",
+ ],
+ libs: [
+ "ddmlib-prebuilt",
+ "error_prone_annotations-2.0.18",
+ "guava",
+ "tradefed-protos",
+ "devtools-annotations-prebuilt",
+ "tradefed-common-util",
+ "tf-remote-client",
+ "tradefed-result-interfaces",
+ ],
+}
+
diff --git a/src/com/android/tradefed/build/BuildInfoKey.java b/device_build_interfaces/com/android/tradefed/build/BuildInfoKey.java
similarity index 100%
rename from src/com/android/tradefed/build/BuildInfoKey.java
rename to device_build_interfaces/com/android/tradefed/build/BuildInfoKey.java
diff --git a/src/com/android/tradefed/build/IBuildInfo.java b/device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
similarity index 87%
rename from src/com/android/tradefed/build/IBuildInfo.java
rename to device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
index d50b8bd..919bc31 100644
--- a/src/com/android/tradefed/build/IBuildInfo.java
+++ b/device_build_interfaces/com/android/tradefed/build/IBuildInfo.java
@@ -21,6 +21,7 @@
import java.io.File;
import java.io.Serializable;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -40,14 +41,18 @@
DO_NOT_COPY_IMAGE_FILE,
}
- /**
- * Default value when build ID is unknown.
- */
- public final static String UNKNOWN_BUILD_ID = "-1";
+ /** Default value when build ID is unknown. */
+ public static final String UNKNOWN_BUILD_ID = "-1";
+
+ /** Prefix used in name to indicate the file is set to be delayed download. */
+ public static final String REMOTE_FILE_PREFIX = "remote_file:";
+
+ /** Remote file is not versioned. */
+ public static final String REMOTE_FILE_VERSION = "";
/**
- * Returns the unique identifier of build under test. Should never be null. Defaults to
- * {@link #UNKNOWN_BUILD_ID}.
+ * Returns the unique identifier of build under test. Should never be null. Defaults to {@link
+ * #UNKNOWN_BUILD_ID}.
*/
public String getBuildId();
@@ -254,6 +259,22 @@
}
/**
+ * Gets a copy of the set of local app apk file(s) and their versions. The returned order
+ * matches the order in which the apks were added to the {@code IAppBuildInfo}.
+ */
+ public default List<VersionedFile> getAppPackageFiles() {
+ return new ArrayList<>();
+ }
+
+ /**
+ * Adds the local apk file and its associated version. Note that apks will be returned from
+ * {@link #getAppPackageFiles()} in the order in which they were added by this method.
+ */
+ public default void addAppPackageFile(File appPackageFile, String version) {
+ // Default implementation for projects that don't extend BuildInfo class.
+ }
+
+ /**
* Clean up any temporary build files
*/
public void cleanUp();
@@ -279,4 +300,9 @@
/** Set the build as test resource build. */
public default void setTestResourceBuild(boolean testResourceBuild) {}
+
+ /** Get the paths for build artifacts that are delayed download. */
+ public default Set<File> getRemoteFiles() {
+ return null;
+ }
}
diff --git a/src/com/android/tradefed/build/VersionedFile.java b/device_build_interfaces/com/android/tradefed/build/VersionedFile.java
similarity index 100%
rename from src/com/android/tradefed/build/VersionedFile.java
rename to device_build_interfaces/com/android/tradefed/build/VersionedFile.java
diff --git a/src/com/android/tradefed/device/AndroidDebugBridgeWrapper.java b/device_build_interfaces/com/android/tradefed/device/AndroidDebugBridgeWrapper.java
similarity index 100%
rename from src/com/android/tradefed/device/AndroidDebugBridgeWrapper.java
rename to device_build_interfaces/com/android/tradefed/device/AndroidDebugBridgeWrapper.java
diff --git a/src/com/android/tradefed/device/DeviceNotAvailableException.java b/device_build_interfaces/com/android/tradefed/device/DeviceNotAvailableException.java
similarity index 100%
rename from src/com/android/tradefed/device/DeviceNotAvailableException.java
rename to device_build_interfaces/com/android/tradefed/device/DeviceNotAvailableException.java
diff --git a/src/com/android/tradefed/device/DeviceRuntimeException.java b/device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
similarity index 100%
rename from src/com/android/tradefed/device/DeviceRuntimeException.java
rename to device_build_interfaces/com/android/tradefed/device/DeviceRuntimeException.java
diff --git a/src/com/android/tradefed/device/IAndroidDebugBridge.java b/device_build_interfaces/com/android/tradefed/device/IAndroidDebugBridge.java
similarity index 100%
rename from src/com/android/tradefed/device/IAndroidDebugBridge.java
rename to device_build_interfaces/com/android/tradefed/device/IAndroidDebugBridge.java
diff --git a/src/com/android/tradefed/device/IDeviceRecovery.java b/device_build_interfaces/com/android/tradefed/device/IDeviceRecovery.java
similarity index 100%
rename from src/com/android/tradefed/device/IDeviceRecovery.java
rename to device_build_interfaces/com/android/tradefed/device/IDeviceRecovery.java
diff --git a/src/com/android/tradefed/device/IDeviceStateMonitor.java b/device_build_interfaces/com/android/tradefed/device/IDeviceStateMonitor.java
similarity index 100%
rename from src/com/android/tradefed/device/IDeviceStateMonitor.java
rename to device_build_interfaces/com/android/tradefed/device/IDeviceStateMonitor.java
diff --git a/src/com/android/tradefed/device/IFileEntry.java b/device_build_interfaces/com/android/tradefed/device/IFileEntry.java
similarity index 100%
rename from src/com/android/tradefed/device/IFileEntry.java
rename to device_build_interfaces/com/android/tradefed/device/IFileEntry.java
diff --git a/src/com/android/tradefed/device/INativeDevice.java b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
similarity index 96%
rename from src/com/android/tradefed/device/INativeDevice.java
rename to device_build_interfaces/com/android/tradefed/device/INativeDevice.java
index 172f0bd..df7420a 100644
--- a/src/com/android/tradefed/device/INativeDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/INativeDevice.java
@@ -40,6 +40,7 @@
import java.util.Collection;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -1275,26 +1276,46 @@
public String getDeviceClass();
/**
- * Extra steps for device specific required setup that will be executed on the device prior
- * to the invocation flow.
+ * Extra steps for device specific required setup that will be executed on the device prior to
+ * the invocation flow.
*/
- public void preInvocationSetup(IBuildInfo info)
- throws TargetSetupError, DeviceNotAvailableException;
+ public default void preInvocationSetup(IBuildInfo info)
+ throws TargetSetupError, DeviceNotAvailableException {
+ preInvocationSetup(info, null);
+ }
/**
* Extra steps for device specific required setup that will be executed on the device prior to
* the invocation flow.
+ *
+ * @param info The {@link IBuildInfo} of the device.
+ * @param testResourceBuildInfos The list of test resources.
+ * @throws TargetSetupError
+ * @throws DeviceNotAvailableException
*/
public default void preInvocationSetup(IBuildInfo info, List<IBuildInfo> testResourceBuildInfos)
throws TargetSetupError, DeviceNotAvailableException {
- preInvocationSetup(info);
+ // Empty default implementation.
}
/**
* Extra steps for device specific required clean up that will be executed after the invocation
* is done.
+ *
+ * @deprecated Use {@link #postInvocationTearDown(Throwable)} instead.
*/
- public void postInvocationTearDown();
+ @Deprecated
+ public default void postInvocationTearDown() {
+ postInvocationTearDown(null);
+ }
+
+ /**
+ * Extra steps for device specific required clean up that will be executed after the invocation
+ * is done.
+ *
+ * @param invocationException if any, the final exception raised by the invocation failure.
+ */
+ public void postInvocationTearDown(Throwable invocationException);
/**
* Return true if the device is headless (no screen), false otherwise.
@@ -1308,20 +1329,32 @@
public DeviceDescriptor getDeviceDescriptor();
/**
- * Helper method runs the "ps" command and returns list of USER, PID and NAME of all the
- * processes.
- *
- * @return List of ProcessInfo objects
- */
- public List<ProcessInfo> getProcesses() throws DeviceNotAvailableException;
-
- /**
- * Helper method runs the "ps" command and returns USER, PID and NAME of the given process name.
+ * Helper method runs the "pidof" and "stat" command and returns {@link ProcessInfo} object with
+ * PID and process start time of the given process.
*
* @return ProcessInfo of given processName
*/
public ProcessInfo getProcessByName(String processName) throws DeviceNotAvailableException;
+ /**
+ * Helper method collects the boot history map with boot time and boot reason.
+ *
+ * @return Map of boot time (UTC time in second since Epoch) and boot reason
+ */
+ public Map<Long, String> getBootHistory() throws DeviceNotAvailableException;
+
+ /**
+ * Helper method collects the boot history map with boot time and boot reason since the given
+ * time in second since epoch from device. The current device utcEpochTime in second can be
+ * obtained by adb shell command "date +%s". Method {@link #getDeviceDate} uses adb shell
+ * command "date +%s" to get device UTC Time since Epoch and return the value in scale of
+ * millisecond.
+ *
+ * @return Map of boot time (UTC time in second since Epoch) and boot reason
+ */
+ public Map<Long, String> getBootHistorySince(long utcEpochTime)
+ throws DeviceNotAvailableException;
+
/** Returns the pid of the service or null if something went wrong. */
public String getProcessPid(String process) throws DeviceNotAvailableException;
@@ -1359,4 +1392,6 @@
* @see <a href="https://source.android.com/devices/tech/debug">Tombstones documentation</a>
*/
public List<File> getTombstones() throws DeviceNotAvailableException;
+
+
}
diff --git a/src/com/android/tradefed/device/ITestDevice.java b/device_build_interfaces/com/android/tradefed/device/ITestDevice.java
similarity index 98%
rename from src/com/android/tradefed/device/ITestDevice.java
rename to device_build_interfaces/com/android/tradefed/device/ITestDevice.java
index 10ecc30..6510852 100644
--- a/src/com/android/tradefed/device/ITestDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/ITestDevice.java
@@ -665,6 +665,16 @@
ArrayList<Integer> listUsers() throws DeviceNotAvailableException;
/**
+ * Gets the Map of useId to {@link UserInfo} on the device. Will throw {@link
+ * DeviceRuntimeException} if output from device is not as expected.
+ *
+ * @return the list of UserInfo objects.
+ * @throws DeviceNotAvailableException
+ * @throws DeviceRuntimeException
+ */
+ public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException;
+
+ /**
* Get the maximum number of supported users. Defaults to 0.
*
* @return an integer indicating the number of supported users
diff --git a/src/com/android/tradefed/device/NullDevice.java b/device_build_interfaces/com/android/tradefed/device/NullDevice.java
similarity index 91%
rename from src/com/android/tradefed/device/NullDevice.java
rename to device_build_interfaces/com/android/tradefed/device/NullDevice.java
index d429264..9973370 100644
--- a/src/com/android/tradefed/device/NullDevice.java
+++ b/device_build_interfaces/com/android/tradefed/device/NullDevice.java
@@ -24,8 +24,7 @@
public class NullDevice extends StubDevice {
/** Naming pattern for auto-created null devices */
- public static final String TEMP_NULL_DEVICE_PREFIX =
- DeviceManager.NULL_DEVICE_SERIAL_PREFIX + "-temp-";
+ public static final String TEMP_NULL_DEVICE_PREFIX = "null-device-temp-";
private boolean mTemporaryDevice = false;
diff --git a/src/com/android/tradefed/device/PackageInfo.java b/device_build_interfaces/com/android/tradefed/device/PackageInfo.java
similarity index 88%
rename from src/com/android/tradefed/device/PackageInfo.java
rename to device_build_interfaces/com/android/tradefed/device/PackageInfo.java
index 1f114d4..fe55ff8 100644
--- a/src/com/android/tradefed/device/PackageInfo.java
+++ b/device_build_interfaces/com/android/tradefed/device/PackageInfo.java
@@ -27,14 +27,17 @@
// frameworks/base/core/java/android/content/pm/ApplicationInfo.java
private static final int FLAG_UPDATED_SYSTEM_APP = 1 << 7;
private static final int FLAG_SYSTEM = 1 << 0;
+ public static final int FLAG_PERSISTENT = 1 << 3;
// string flag constants. Used for newer platforms
private static final String FLAG_UPDATED_SYSTEM_APP_TEXT = " UPDATED_SYSTEM_APP ";
private static final String FLAG_SYSTEM_TEXT = " SYSTEM ";
+ private static final String FLAG_PERSISTENT_TEXT = " PERSISTENT ";
private final String mPackageName;
private boolean mIsSystemApp;
private boolean mIsUpdatedSystemApp;
+ private boolean mIsPersistentApp;
private Map<String, String> mAttributes = new HashMap<String, String>();
PackageInfo(String pkgName) {
@@ -56,6 +59,13 @@
}
/**
+ * Returns <code>true</code> if this is a persistent app.
+ */
+ public boolean isPersistentApp() {
+ return mIsPersistentApp;
+ }
+
+ /**
* Returns the package name of the application.
*/
public String getPackageName() {
@@ -89,6 +99,7 @@
private void parseFlagsAsString(String flagString) {
mIsSystemApp = flagString.contains(FLAG_SYSTEM_TEXT);
mIsUpdatedSystemApp = flagString.contains(FLAG_UPDATED_SYSTEM_APP_TEXT);
+ mIsPersistentApp = flagString.contains(FLAG_PERSISTENT_TEXT);
}
private boolean parseFlagsAsInt(String value) {
@@ -98,6 +109,7 @@
// note: FLAG_UPDATED_SYSTEM_APP never seems to be set. Rely on parsing hidden system
// packages
mIsUpdatedSystemApp = (flags & FLAG_UPDATED_SYSTEM_APP) != 0;
+ mIsPersistentApp = (flags & FLAG_PERSISTENT) != 0;
return true;
} catch (NumberFormatException e) {
// not an int, fall through
@@ -105,3 +117,4 @@
return false;
}
}
+
diff --git a/src/com/android/tradefed/device/StubDevice.java b/device_build_interfaces/com/android/tradefed/device/StubDevice.java
similarity index 100%
rename from src/com/android/tradefed/device/StubDevice.java
rename to device_build_interfaces/com/android/tradefed/device/StubDevice.java
diff --git a/src/com/android/tradefed/device/TcpDevice.java b/device_build_interfaces/com/android/tradefed/device/TcpDevice.java
similarity index 100%
rename from src/com/android/tradefed/device/TcpDevice.java
rename to device_build_interfaces/com/android/tradefed/device/TcpDevice.java
diff --git a/src/com/android/tradefed/device/TestDeviceOptions.java b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
similarity index 96%
rename from src/com/android/tradefed/device/TestDeviceOptions.java
rename to device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
index be94963..eb62635 100644
--- a/src/com/android/tradefed/device/TestDeviceOptions.java
+++ b/device_build_interfaces/com/android/tradefed/device/TestDeviceOptions.java
@@ -247,6 +247,17 @@
)
private Set<String> mRemoteFetchFilePattern = new HashSet<>();
+ @Option(name = "cros-user", description = "(CHEEPS ONLY) Account to log in to Chrome OS with.")
+ private String mCrosUser = null;
+
+ @Option(
+ name = "cros-password",
+ description =
+ "(CHEEPS ONLY) Password to log in to Chrome OS with. Only used if cros-user "
+ + "is specified."
+ )
+ private String mCrosPassword = null;
+
// END ====================== Options Related to Virtual Devices ======================
/** Check whether adb root should be enabled on boot for this device */
@@ -616,6 +627,16 @@
return mRemoteFetchFilePattern;
}
+ /** Returns the Chrome OS User to log in as. */
+ public String getCrosUser() {
+ return mCrosUser;
+ }
+
+ /** Returns the password to log in to Chrome OS with. */
+ public String getCrosPassword() {
+ return mCrosPassword;
+ }
+
public static String getCreateCommandByInstanceType(InstanceType type) {
switch (type) {
case CHEEPS:
diff --git a/src/com/android/tradefed/device/TestDeviceState.java b/device_build_interfaces/com/android/tradefed/device/TestDeviceState.java
similarity index 100%
rename from src/com/android/tradefed/device/TestDeviceState.java
rename to device_build_interfaces/com/android/tradefed/device/TestDeviceState.java
diff --git a/device_build_interfaces/com/android/tradefed/device/UserInfo.java b/device_build_interfaces/com/android/tradefed/device/UserInfo.java
new file mode 100644
index 0000000..4cccbbb
--- /dev/null
+++ b/device_build_interfaces/com/android/tradefed/device/UserInfo.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device;
+
+/**
+ * Similar to UserInfo class from platform.
+ *
+ * <p>This is intended to be similar to android.content.pm.UserInfo.
+ *
+ * <p>Stores data and basic logic around the information for one user.
+ */
+public final class UserInfo {
+ // From android.content.pm.UserInfo
+ public static final int FLAG_PRIMARY = 0x00000001;
+ public static final int FLAG_GUEST = 0x00000004;
+ public static final int FLAG_RESTRICTED = 0x00000008;
+ public static final int FLAG_MANAGED_PROFILE = 0x00000020;
+ public static final int USER_SYSTEM = 0;
+
+ public static final int FLAGS_NOT_SECONDARY =
+ FLAG_PRIMARY | FLAG_MANAGED_PROFILE | FLAG_GUEST | FLAG_RESTRICTED;
+
+ private final int mUserId;
+ private final String mUserName;
+ private final int mFlag;
+ private final boolean mIsRunning;
+
+ /** Supported variants of a user's type in external APIs. */
+ public enum UserType {
+ /** current foreground user of the device */
+ CURRENT,
+ /**
+ * guest user. Only one can exist at a time, may be ephemeral and have more restrictions.
+ */
+ GUEST,
+ /** user flagged as primary on the device; most often primary = system user = user 0 */
+ PRIMARY,
+ /** system user = user 0 */
+ SYSTEM,
+ /** secondary user, i.e. non-primary and non-system. */
+ SECONDARY;
+
+ public boolean isCurrent() {
+ return this == CURRENT;
+ }
+
+ public boolean isGuest() {
+ return this == GUEST;
+ }
+
+ public boolean isPrimary() {
+ return this == PRIMARY;
+ }
+
+ public boolean isSystem() {
+ return this == SYSTEM;
+ }
+
+ public boolean isSecondary() {
+ return this == SECONDARY;
+ }
+ }
+
+ public UserInfo(int userId, String userName, int flag, boolean isRunning) {
+ mUserId = userId;
+ mUserName = userName;
+ mFlag = flag;
+ mIsRunning = isRunning;
+ }
+
+ public int userId() {
+ return mUserId;
+ }
+
+ public String userName() {
+ return mUserName;
+ }
+
+ public int flag() {
+ return mFlag;
+ }
+
+ public boolean isRunning() {
+ return mIsRunning;
+ }
+
+ public boolean isGuest() {
+ return (mFlag & FLAG_GUEST) == FLAG_GUEST;
+ }
+
+ public boolean isPrimary() {
+ return (mFlag & FLAG_PRIMARY) == FLAG_PRIMARY;
+ }
+
+ public boolean isSecondary() {
+ return !isSystem() && (mFlag & FLAGS_NOT_SECONDARY) == 0;
+ }
+
+ public boolean isSystem() {
+ return mUserId == USER_SYSTEM;
+ }
+
+ /** Return whether this instance is of the specified type. */
+ public boolean isUserType(UserType userType, int currentUserId) {
+ switch (userType) {
+ case CURRENT:
+ return mUserId == currentUserId;
+ case GUEST:
+ return isGuest();
+ case PRIMARY:
+ return isPrimary();
+ case SYSTEM:
+ return isSystem();
+ case SECONDARY:
+ return isSecondary();
+ default:
+ throw new RuntimeException("Variant not covered: " + userType);
+ }
+ }
+}
diff --git a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
similarity index 100%
rename from src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
rename to device_build_interfaces/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
diff --git a/src/com/android/tradefed/targetprep/TargetSetupError.java b/device_build_interfaces/com/android/tradefed/targetprep/TargetSetupError.java
similarity index 100%
rename from src/com/android/tradefed/targetprep/TargetSetupError.java
rename to device_build_interfaces/com/android/tradefed/targetprep/TargetSetupError.java
diff --git a/src/com/android/tradefed/util/Bugreport.java b/device_build_interfaces/com/android/tradefed/util/Bugreport.java
similarity index 100%
rename from src/com/android/tradefed/util/Bugreport.java
rename to device_build_interfaces/com/android/tradefed/util/Bugreport.java
diff --git a/src/com/android/tradefed/util/KeyguardControllerState.java b/device_build_interfaces/com/android/tradefed/util/KeyguardControllerState.java
similarity index 100%
rename from src/com/android/tradefed/util/KeyguardControllerState.java
rename to device_build_interfaces/com/android/tradefed/util/KeyguardControllerState.java
diff --git a/device_build_interfaces/com/android/tradefed/util/ProcessInfo.java b/device_build_interfaces/com/android/tradefed/util/ProcessInfo.java
new file mode 100644
index 0000000..158af60
--- /dev/null
+++ b/device_build_interfaces/com/android/tradefed/util/ProcessInfo.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+/** Used to store process related(USER, PID, NAME, START TIME IN SECOND SINCE EPOCH) information. */
+public class ProcessInfo {
+
+ private String mUser;
+ private int mPid;
+ private String mName;
+ private long mStartTime;
+
+ /**
+ * Constructs the process info object based on the user, process id and name of the process.
+ *
+ * @param user username of process owner
+ * @param pid process id number
+ * @param name process name
+ */
+ public ProcessInfo(String user, int pid, String name) {
+ mUser = user;
+ mPid = pid;
+ mName = name;
+ }
+
+ /**
+ * Constructs the process info object based on the user, process id, name of the process,
+ * process start time.
+ *
+ * @param user username of process owner
+ * @param pid process id number
+ * @param name process name
+ * @param startTime process start time in second since epoch
+ */
+ public ProcessInfo(String user, int pid, String name, long startTime) {
+ mUser = user;
+ mPid = pid;
+ mName = name;
+ mStartTime = startTime;
+ }
+
+ /** Returns the username of the process's owner. */
+ public String getUser() {
+ return mUser;
+ }
+
+ /** Returns the process ID number. */
+ public int getPid() {
+ return mPid;
+ }
+
+ /** Returns the process name. */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the process start time in second since epoch. For device process, the start time
+ * would use device time
+ */
+ public long getStartTime() {
+ return mStartTime;
+ }
+}
+
diff --git a/global_configuration/OWNERS b/global_configuration/OWNERS
new file mode 100644
index 0000000..6802673
--- /dev/null
+++ b/global_configuration/OWNERS
@@ -0,0 +1,4 @@
+# host/ drives host related setup or configuration: base OWNERS +
+fangk@google.com
+jeffreylu@google.com
+xingdai@google.com
diff --git a/src/com/android/tradefed/config/GlobalConfiguration.java b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
similarity index 99%
rename from src/com/android/tradefed/config/GlobalConfiguration.java
rename to global_configuration/com/android/tradefed/config/GlobalConfiguration.java
index daf6ebc..e3f0142 100644
--- a/src/com/android/tradefed/config/GlobalConfiguration.java
+++ b/global_configuration/com/android/tradefed/config/GlobalConfiguration.java
@@ -95,6 +95,7 @@
DEVICE_MANAGER_TYPE_NAME,
KEY_STORE_TYPE_NAME,
HOST_OPTIONS_TYPE_NAME,
+ DynamicRemoteFileResolver.DYNAMIC_RESOLVER,
"android-build"
};
@@ -802,7 +803,7 @@
isGenericObject = true;
}
ConfigurationUtil.dumpClassToXml(
- serializer, config, configObj, isGenericObject, new ArrayList<>(), true);
+ serializer, config, configObj, isGenericObject, new ArrayList<>(), true, true);
}
serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
serializer.endDocument();
diff --git a/src/com/android/tradefed/config/IConfigurationServer.java b/global_configuration/com/android/tradefed/config/IConfigurationServer.java
similarity index 100%
rename from src/com/android/tradefed/config/IConfigurationServer.java
rename to global_configuration/com/android/tradefed/config/IConfigurationServer.java
diff --git a/src/com/android/tradefed/config/IGlobalConfiguration.java b/global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
similarity index 100%
rename from src/com/android/tradefed/config/IGlobalConfiguration.java
rename to global_configuration/com/android/tradefed/config/IGlobalConfiguration.java
diff --git a/src/com/android/tradefed/config/gcs/GCSConfigurationFactory.java b/global_configuration/com/android/tradefed/config/gcs/GCSConfigurationFactory.java
similarity index 100%
rename from src/com/android/tradefed/config/gcs/GCSConfigurationFactory.java
rename to global_configuration/com/android/tradefed/config/gcs/GCSConfigurationFactory.java
diff --git a/src/com/android/tradefed/config/gcs/GCSConfigurationServer.java b/global_configuration/com/android/tradefed/config/gcs/GCSConfigurationServer.java
similarity index 100%
rename from src/com/android/tradefed/config/gcs/GCSConfigurationServer.java
rename to global_configuration/com/android/tradefed/config/gcs/GCSConfigurationServer.java
diff --git a/src/com/android/tradefed/host/HostOptions.java b/global_configuration/com/android/tradefed/host/HostOptions.java
similarity index 100%
rename from src/com/android/tradefed/host/HostOptions.java
rename to global_configuration/com/android/tradefed/host/HostOptions.java
diff --git a/src/com/android/tradefed/host/IHostOptions.java b/global_configuration/com/android/tradefed/host/IHostOptions.java
similarity index 100%
rename from src/com/android/tradefed/host/IHostOptions.java
rename to global_configuration/com/android/tradefed/host/IHostOptions.java
diff --git a/src/com/android/tradefed/host/IHostResourceManager.java b/global_configuration/com/android/tradefed/host/IHostResourceManager.java
similarity index 100%
rename from src/com/android/tradefed/host/IHostResourceManager.java
rename to global_configuration/com/android/tradefed/host/IHostResourceManager.java
diff --git a/src/com/android/tradefed/host/LocalHostResourceManager.java b/global_configuration/com/android/tradefed/host/LocalHostResourceManager.java
similarity index 100%
rename from src/com/android/tradefed/host/LocalHostResourceManager.java
rename to global_configuration/com/android/tradefed/host/LocalHostResourceManager.java
diff --git a/src/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java b/global_configuration/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
rename to global_configuration/com/android/tradefed/util/hostmetric/AbstractHostMonitor.java
diff --git a/src/com/android/tradefed/util/hostmetric/EmailHostHealthAgent.java b/global_configuration/com/android/tradefed/util/hostmetric/EmailHostHealthAgent.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/EmailHostHealthAgent.java
rename to global_configuration/com/android/tradefed/util/hostmetric/EmailHostHealthAgent.java
diff --git a/src/com/android/tradefed/util/hostmetric/HeapHostMonitor.java b/global_configuration/com/android/tradefed/util/hostmetric/HeapHostMonitor.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/HeapHostMonitor.java
rename to global_configuration/com/android/tradefed/util/hostmetric/HeapHostMonitor.java
diff --git a/src/com/android/tradefed/util/hostmetric/HostMetric.java b/global_configuration/com/android/tradefed/util/hostmetric/HostMetric.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/HostMetric.java
rename to global_configuration/com/android/tradefed/util/hostmetric/HostMetric.java
diff --git a/src/com/android/tradefed/util/hostmetric/IHostHealthAgent.java b/global_configuration/com/android/tradefed/util/hostmetric/IHostHealthAgent.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/IHostHealthAgent.java
rename to global_configuration/com/android/tradefed/util/hostmetric/IHostHealthAgent.java
diff --git a/src/com/android/tradefed/util/hostmetric/IHostMonitor.java b/global_configuration/com/android/tradefed/util/hostmetric/IHostMonitor.java
similarity index 100%
rename from src/com/android/tradefed/util/hostmetric/IHostMonitor.java
rename to global_configuration/com/android/tradefed/util/hostmetric/IHostMonitor.java
diff --git a/src/com/android/tradefed/util/keystore/DryRunKeyStore.java b/global_configuration/com/android/tradefed/util/keystore/DryRunKeyStore.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/DryRunKeyStore.java
rename to global_configuration/com/android/tradefed/util/keystore/DryRunKeyStore.java
diff --git a/src/com/android/tradefed/util/keystore/IKeyStoreClient.java b/global_configuration/com/android/tradefed/util/keystore/IKeyStoreClient.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/IKeyStoreClient.java
rename to global_configuration/com/android/tradefed/util/keystore/IKeyStoreClient.java
diff --git a/src/com/android/tradefed/util/keystore/IKeyStoreFactory.java b/global_configuration/com/android/tradefed/util/keystore/IKeyStoreFactory.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/IKeyStoreFactory.java
rename to global_configuration/com/android/tradefed/util/keystore/IKeyStoreFactory.java
diff --git a/src/com/android/tradefed/util/keystore/JSONFileKeyStoreClient.java b/global_configuration/com/android/tradefed/util/keystore/JSONFileKeyStoreClient.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/JSONFileKeyStoreClient.java
rename to global_configuration/com/android/tradefed/util/keystore/JSONFileKeyStoreClient.java
diff --git a/src/com/android/tradefed/util/keystore/JSONFileKeyStoreFactory.java b/global_configuration/com/android/tradefed/util/keystore/JSONFileKeyStoreFactory.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/JSONFileKeyStoreFactory.java
rename to global_configuration/com/android/tradefed/util/keystore/JSONFileKeyStoreFactory.java
diff --git a/src/com/android/tradefed/util/keystore/KeyStoreException.java b/global_configuration/com/android/tradefed/util/keystore/KeyStoreException.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/KeyStoreException.java
rename to global_configuration/com/android/tradefed/util/keystore/KeyStoreException.java
diff --git a/src/com/android/tradefed/util/keystore/StubKeyStoreClient.java b/global_configuration/com/android/tradefed/util/keystore/StubKeyStoreClient.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/StubKeyStoreClient.java
rename to global_configuration/com/android/tradefed/util/keystore/StubKeyStoreClient.java
diff --git a/src/com/android/tradefed/util/keystore/StubKeyStoreFactory.java b/global_configuration/com/android/tradefed/util/keystore/StubKeyStoreFactory.java
similarity index 100%
rename from src/com/android/tradefed/util/keystore/StubKeyStoreFactory.java
rename to global_configuration/com/android/tradefed/util/keystore/StubKeyStoreFactory.java
diff --git a/invocation_interfaces/Android.bp b/invocation_interfaces/Android.bp
new file mode 100644
index 0000000..5208e51
--- /dev/null
+++ b/invocation_interfaces/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+ name: "tradefed-invocation-interfaces",
+ defaults: ["tradefed_defaults"],
+ srcs: [
+ "com/**/*.java",
+ ],
+ libs: [
+ "guava",
+ "tradefed-common-util",
+ "tradefed-protos",
+ "tradefed-result-interfaces",
+ "tradefed-device-build-interfaces",
+ ],
+}
+
diff --git a/src/com/android/tradefed/config/ConfigurationDescriptor.java b/invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
similarity index 98%
rename from src/com/android/tradefed/config/ConfigurationDescriptor.java
rename to invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
index 8d948d2..bc6d30c 100644
--- a/src/com/android/tradefed/config/ConfigurationDescriptor.java
+++ b/invocation_interfaces/com/android/tradefed/config/ConfigurationDescriptor.java
@@ -16,7 +16,6 @@
package com.android.tradefed.config;
import com.android.tradefed.build.BuildSerializedVersion;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
import com.android.tradefed.config.proto.ConfigurationDescription;
import com.android.tradefed.config.proto.ConfigurationDescription.Descriptor;
import com.android.tradefed.config.proto.ConfigurationDescription.Metadata;
@@ -48,6 +47,8 @@
/** Metadata key for a config to specify that it was sharded. */
public static final String LOCAL_SHARDED_KEY = "sharded";
+ /** Metadata key for a config parameterization, optional. */
+ public static final String PARAMETER_KEY = "parameter";
@Option(name = "test-suite-tag", description = "A membership tag to suite. Can be repeated.")
private List<String> mSuiteTags = new ArrayList<>();
diff --git a/src/com/android/tradefed/invoker/IInvocationContext.java b/invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
similarity index 95%
rename from src/com/android/tradefed/invoker/IInvocationContext.java
rename to invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
index 9175550..537daf4 100644
--- a/src/com/android/tradefed/invoker/IInvocationContext.java
+++ b/invocation_interfaces/com/android/tradefed/invoker/IInvocationContext.java
@@ -19,7 +19,6 @@
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
-import com.android.tradefed.testtype.suite.ITestSuite;
import com.android.tradefed.util.MultiMap;
import com.android.tradefed.util.UniqueMultiMap;
@@ -159,14 +158,10 @@
*/
public ConfigurationDescriptor getConfigurationDescriptor();
- /**
- * Sets the invocation context of module while being executed as part of a {@link ITestSuite}
- */
+ /** Sets the invocation context of module while being executed as part of a suite. */
public void setModuleInvocationContext(IInvocationContext invocationContext);
- /**
- * Returns the invocation context of module while being executed as part of a {@link ITestSuite}
- */
+ /** Returns the invocation context of module while being executed as part of a suite. */
public IInvocationContext getModuleInvocationContext();
/** Returns the invocation test-tag. */
diff --git a/src/com/android/tradefed/result/ILogSaver.java b/invocation_interfaces/com/android/tradefed/result/ILogSaver.java
similarity index 100%
rename from src/com/android/tradefed/result/ILogSaver.java
rename to invocation_interfaces/com/android/tradefed/result/ILogSaver.java
diff --git a/src/com/android/tradefed/result/ILogSaverListener.java b/invocation_interfaces/com/android/tradefed/result/ILogSaverListener.java
similarity index 100%
rename from src/com/android/tradefed/result/ILogSaverListener.java
rename to invocation_interfaces/com/android/tradefed/result/ILogSaverListener.java
diff --git a/src/com/android/tradefed/result/ITestInvocationListener.java b/invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
similarity index 89%
rename from src/com/android/tradefed/result/ITestInvocationListener.java
rename to invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
index 72ff5ad..e6f5357 100644
--- a/src/com/android/tradefed/result/ITestInvocationListener.java
+++ b/invocation_interfaces/com/android/tradefed/result/ITestInvocationListener.java
@@ -15,10 +15,8 @@
*/
package com.android.tradefed.result;
-import com.android.tradefed.command.ICommandScheduler;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.ITestLogger;
-import com.android.tradefed.testtype.suite.ITestSuite;
/**
* Listener for test results from the test invocation.
@@ -82,17 +80,17 @@
default public TestSummary getSummary() { return null; }
/**
- * Called on {@link ICommandScheduler#shutdown()}, gives the invocation the opportunity to do
- * something before terminating.
+ * Called on scheduler shutdown, gives the invocation the opportunity to do something before
+ * terminating.
*/
- default public void invocationInterrupted() {
+ public default void invocationInterrupted() {
// do nothing in default implementation.
}
/**
* Reports the beginning of a module running. This callback is associated with {@link
* #testModuleEnded()} and is optional in the sequence. It is only used during a run that uses
- * modules: {@link ITestSuite} based runners.
+ * modules: suite based runners.
*
* @param moduleContext the {@link IInvocationContext} of the module.
*/
diff --git a/src/com/android/tradefed/result/LogFile.java b/invocation_interfaces/com/android/tradefed/result/LogFile.java
similarity index 100%
rename from src/com/android/tradefed/result/LogFile.java
rename to invocation_interfaces/com/android/tradefed/result/LogFile.java
diff --git a/src/com/android/tradefed/result/TestSummary.java b/invocation_interfaces/com/android/tradefed/result/TestSummary.java
similarity index 100%
rename from src/com/android/tradefed/result/TestSummary.java
rename to invocation_interfaces/com/android/tradefed/result/TestSummary.java
diff --git a/remote/.classpath b/remote/.classpath
index 74e2efb..f7e5ee7 100644
--- a/remote/.classpath
+++ b/remote/.classpath
@@ -1,15 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
- <classpathentry kind="src" path="guava-failureaccess"/>
- <classpathentry kind="src" path="guava-annot"/>
- <classpathentry kind="src" path="jsr305"/>
- <classpathentry kind="src" path="guava"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/json/json-prebuilt.jar"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/sdklib/sdklib-prebuilt.jar"/>
<classpathentry exported="true" kind="var" path="TRADEFED_ROOT/prebuilts/misc/common/tools-common/tools-common-prebuilt.jar"/>
<classpathentry combineaccessrules="false" kind="src" path="/ddmlib"/>
<classpathentry kind="var" path="TRADEFED_ROOT/external/error_prone/error_prone/error_prone_annotations-2.3.2.jar"/>
+ <classpathentry kind="var" path="TRADEFED_ROOT/out/soong/.intermediates/external/guava/guava-jre/linux_glibc_common/combined/guava-jre.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/remote/.project b/remote/.project
index d5451e0..39f7e18 100644
--- a/remote/.project
+++ b/remote/.project
@@ -14,26 +14,4 @@
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
- <linkedResources>
- <link>
- <name>guava</name>
- <type>2</type>
- <locationURI>TRADEFED_ROOT/external/guava/guava/src</locationURI>
- </link>
- <link>
- <name>guava-annot</name>
- <type>2</type>
- <locationURI>TRADEFED_ROOT/external/guava/android-annotation-stubs/src</locationURI>
- </link>
- <link>
- <name>guava-failureaccess</name>
- <type>2</type>
- <location>/usr/local/google/home/jdesprez/aosp/external/guava/futures/failureaccess/src</location>
- </link>
- <link>
- <name>jsr305</name>
- <type>2</type>
- <locationURI>TRADEFED_ROOT/external/jsr305/ri/src/main/java</locationURI>
- </link>
- </linkedResources>
</projectDescription>
diff --git a/res/apks/wifiutil/PREBUILT b/res/apks/wifiutil/PREBUILT
index 935017a..6475d73 100644
--- a/res/apks/wifiutil/PREBUILT
+++ b/res/apks/wifiutil/PREBUILT
@@ -1,4 +1,4 @@
This apk can be rebuilt from
platform/tools/tradefederation/core
-By running `m WifiUtil` on revision 2514302f05212870a8fef077bd378a3328571d69
+By running `m WifiUtil` on revision d6279aed8f7ea57530dea2c72a6e86b3bd11462a
diff --git a/res/apks/wifiutil/WifiUtil.apk b/res/apks/wifiutil/WifiUtil.apk
index e5e6fc0..7a98f1c 100644
--- a/res/apks/wifiutil/WifiUtil.apk
+++ b/res/apks/wifiutil/WifiUtil.apk
Binary files differ
diff --git a/res/config/testdef.xml b/res/config/instrumentations.xml
similarity index 60%
rename from res/config/testdef.xml
rename to res/config/instrumentations.xml
index d2e0fe3..88c7d50 100644
--- a/res/config/testdef.xml
+++ b/res/config/instrumentations.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2010 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,10 +13,12 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<configuration
- description="Runs tests contained in test_def.xml files on an existing device">
+<configuration description="Runs all the Android instrumentation tests on an existing device">
- <test class="com.android.tradefed.testtype.testdefs.XmlDefsTest" />
- <logger class="com.android.tradefed.log.FileLogger" />
- <result_reporter class="com.android.tradefed.result.XmlResultReporter" />
+ <target_preparer class="com.android.tradefed.targetprep.InstallApkSetup">
+ <!-- Use "apk-path" option to specify which apk to install -->
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.InstalledInstrumentationsTest" />
+
</configuration>
diff --git a/src/com/android/tradefed/build/AppBuildInfo.java b/src/com/android/tradefed/build/AppBuildInfo.java
index b421f2e..402674f 100644
--- a/src/com/android/tradefed/build/AppBuildInfo.java
+++ b/src/com/android/tradefed/build/AppBuildInfo.java
@@ -16,12 +16,6 @@
package com.android.tradefed.build;
-import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* A {@link IBuildInfo} that represents an Android application and its test package(s).
*/
@@ -45,25 +39,4 @@
public AppBuildInfo(BuildInfo buildToCopy) {
super(buildToCopy);
}
-
- /**
- * {@inheritDoc}
- */
- @Override
- public List<VersionedFile> getAppPackageFiles() {
- List<VersionedFile> origList = getVersionedFiles(BuildInfoFileKey.PACKAGE_FILES);
- List<VersionedFile> listCopy = new ArrayList<VersionedFile>();
- if (origList != null) {
- listCopy.addAll(origList);
- }
- return listCopy;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void addAppPackageFile(File appPackageFile, String version) {
- setFile(BuildInfoFileKey.PACKAGE_FILES, appPackageFile, version);
- }
}
diff --git a/src/com/android/tradefed/build/AppDeviceBuildInfo.java b/src/com/android/tradefed/build/AppDeviceBuildInfo.java
index 5ccef4e..78d6f72 100644
--- a/src/com/android/tradefed/build/AppDeviceBuildInfo.java
+++ b/src/com/android/tradefed/build/AppDeviceBuildInfo.java
@@ -16,13 +16,12 @@
package com.android.tradefed.build;
-import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-/** A {@link IDeviceBuildInfo} that also contains a {@link IAppBuildInfo}. */
+/**
+ * A {@link IDeviceBuildInfo} that also contains a {@link IAppBuildInfo}.
+ *
+ * @deprecated Use {@link IDeviceBuildInfo} directly.
+ */
+@Deprecated
public class AppDeviceBuildInfo extends DeviceBuildInfo implements IAppBuildInfo {
private static final long serialVersionUID = BuildSerializedVersion.VERSION;
@@ -34,27 +33,6 @@
super(buildId, buildName);
}
- /**
- * {@inheritDoc}
- */
- @Override
- public void addAppPackageFile(File appPackageFile, String version) {
- setFile(BuildInfoFileKey.PACKAGE_FILES, appPackageFile, version);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public List<VersionedFile> getAppPackageFiles() {
- List<VersionedFile> origList = getVersionedFiles(BuildInfoFileKey.PACKAGE_FILES);
- List<VersionedFile> listCopy = new ArrayList<VersionedFile>();
- if (origList != null) {
- listCopy.addAll(origList);
- }
- return listCopy;
- }
-
/** Copy all the files from the {@link IAppBuildInfo}. */
public void setAppBuild(IAppBuildInfo appBuild) {
copyAllFileFrom((BuildInfo) appBuild);
diff --git a/src/com/android/tradefed/build/BootstrapBuildProvider.java b/src/com/android/tradefed/build/BootstrapBuildProvider.java
index c6cf792..5da8557 100644
--- a/src/com/android/tradefed/build/BootstrapBuildProvider.java
+++ b/src/com/android/tradefed/build/BootstrapBuildProvider.java
@@ -22,6 +22,7 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.StubDevice;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.BuildInfoUtil;
import com.android.tradefed.util.FileUtil;
import java.io.File;
@@ -97,12 +98,7 @@
@Override
public IBuildInfo getBuild(ITestDevice device) throws BuildRetrievalError,
DeviceNotAvailableException {
- String buildId = mBuildId;
- // If mBuildId is set, do not use the device build-id
- if (buildId == null) {
- buildId = device.getBuildId();
- }
- IBuildInfo info = new DeviceBuildInfo(buildId, mBuildTargetName);
+ IBuildInfo info = new DeviceBuildInfo(mBuildId, mBuildTargetName);
if (!(device.getIDevice() instanceof StubDevice)) {
if (!device.waitForDeviceShell(mShellAvailableTimeout * 1000)) {
throw new DeviceNotAvailableException(
@@ -111,24 +107,19 @@
mShellAvailableTimeout),
device.getSerialNumber());
}
- if (mBranch == null) {
- mBranch =
- String.format(
- "%s-%s-%s-%s",
- device.getProperty("ro.product.brand"),
- device.getProperty("ro.product.name"),
- device.getProductVariant(),
- device.getProperty("ro.build.version.release"));
- }
} else {
// In order to avoid issue with a null branch, use a placeholder stub for StubDevice.
mBranch = "stub";
}
- info.setBuildBranch(mBranch);
- info.setBuildFlavor(device.getBuildFlavor());
- info.addBuildAttribute("build_alias", device.getBuildAlias());
+ BuildInfoUtil.bootstrapDeviceBuildAttributes(
+ info,
+ device,
+ mBuildId,
+ null /* override build flavor */,
+ mBranch,
+ null /* override build alias */);
if (mTestsDir != null && mTestsDir.isDirectory()) {
- info.setFile("testsdir", mTestsDir, buildId);
+ info.setFile("testsdir", mTestsDir, info.getBuildId());
}
// Avoid tests dir being null, by creating a temporary dir.
if (mTestsDir == null) {
diff --git a/src/com/android/tradefed/build/BuildInfo.java b/src/com/android/tradefed/build/BuildInfo.java
index 729e6ca..f1a92fd 100644
--- a/src/com/android/tradefed/build/BuildInfo.java
+++ b/src/com/android/tradefed/build/BuildInfo.java
@@ -33,6 +33,7 @@
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
@@ -390,6 +391,23 @@
setFile(key.getFileKey(), file, version);
}
+ /** {@inheritDoc} */
+ @Override
+ public List<VersionedFile> getAppPackageFiles() {
+ List<VersionedFile> origList = getVersionedFiles(BuildInfoFileKey.PACKAGE_FILES);
+ List<VersionedFile> listCopy = new ArrayList<VersionedFile>();
+ if (origList != null) {
+ listCopy.addAll(origList);
+ }
+ return listCopy;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void addAppPackageFile(File appPackageFile, String version) {
+ setFile(BuildInfoFileKey.PACKAGE_FILES, appPackageFile, version);
+ }
+
/**
* {@inheritDoc}
*/
@@ -526,13 +544,13 @@
return false;
}
BuildInfo other = (BuildInfo) obj;
- return Objects.equal(mBuildAttributes, other.mBuildAttributes) &&
- Objects.equal(mBuildBranch, other.mBuildBranch) &&
- Objects.equal(mBuildFlavor, other.mBuildFlavor) &&
- Objects.equal(mBuildId, other.mBuildId) &&
- Objects.equal(mBuildTargetName, other.mBuildTargetName) &&
- Objects.equal(mTestTag, other.mTestTag) &&
- Objects.equal(mDeviceSerial, other.mDeviceSerial);
+ return Objects.equal(mBuildAttributes, other.mBuildAttributes)
+ && Objects.equal(mBuildBranch, other.mBuildBranch)
+ && Objects.equal(mBuildFlavor, other.mBuildFlavor)
+ && Objects.equal(mBuildId, other.mBuildId)
+ && Objects.equal(mBuildTargetName, other.mBuildTargetName)
+ && Objects.equal(mTestTag, other.mTestTag)
+ && Objects.equal(mDeviceSerial, other.mDeviceSerial);
}
/**
@@ -573,7 +591,12 @@
for (VersionedFile vFile : mVersionedFileMultiMap.get(fileKey)) {
BuildFile.Builder fileInformation = BuildFile.newBuilder();
fileInformation.setVersion(vFile.getVersion());
- fileInformation.setLocalPath(vFile.getFile().getAbsolutePath());
+ if (fileKey.startsWith(IBuildInfo.REMOTE_FILE_PREFIX)) {
+ // Remote file doesn't exist on local cache, so don't save absolute path.
+ fileInformation.setLocalPath(vFile.getFile().toString());
+ } else {
+ fileInformation.setLocalPath(vFile.getFile().getAbsolutePath());
+ }
buildFile.addFile(fileInformation);
}
protoBuilder.addVersionedFile(buildFile);
@@ -674,4 +697,18 @@
}
return null;
}
+
+
+ /** {@inheritDoc} */
+ @Override
+ public Set<File> getRemoteFiles() {
+ Set<File> remoteFiles = new HashSet<>();
+ for (String fileKey : mVersionedFileMultiMap.keySet()) {
+ if (fileKey.startsWith(IBuildInfo.REMOTE_FILE_PREFIX)) {
+ // Remote file is not versioned, there should be only one entry.
+ remoteFiles.add(mVersionedFileMultiMap.get(fileKey).get(0).getFile());
+ }
+ }
+ return remoteFiles;
+ }
}
diff --git a/src/com/android/tradefed/build/FileDownloadCacheWrapper.java b/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
index 606b2a4..ecf3f44 100644
--- a/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
+++ b/src/com/android/tradefed/build/FileDownloadCacheWrapper.java
@@ -16,6 +16,8 @@
package com.android.tradefed.build;
import java.io.File;
+import java.io.IOException;
+import java.util.List;
/**
* A wrapper class that provides {@link FileDownloadCache} facilities while implementing the
@@ -54,4 +56,16 @@
public boolean isFresh(File localFile, String remoteFilePath) throws BuildRetrievalError {
return mDelegateDownloader.isFresh(localFile, remoteFilePath);
}
+
+ /** {@inheritDoc} */
+ @Override
+ public void downloadPartialFiles(
+ File destDir,
+ String remoteFilePath,
+ List<String> includeFilters,
+ List<String> excludeFilters)
+ throws BuildRetrievalError, IOException {
+ mDelegateDownloader.downloadPartialFiles(
+ destDir, remoteFilePath, includeFilters, excludeFilters);
+ }
}
diff --git a/src/com/android/tradefed/build/IAppBuildInfo.java b/src/com/android/tradefed/build/IAppBuildInfo.java
index fead9d7..71e7bf1 100644
--- a/src/com/android/tradefed/build/IAppBuildInfo.java
+++ b/src/com/android/tradefed/build/IAppBuildInfo.java
@@ -16,24 +16,10 @@
package com.android.tradefed.build;
-import java.io.File;
-import java.util.List;
-
/**
- * * A {@link IBuildInfo} that represents an Android application and its test package(s).
+ * A {@link IBuildInfo} that represents an Android application and its test package(s).
+ *
+ * @deprecated Use {@link IBuildInfo} directly.
*/
-public interface IAppBuildInfo extends IBuildInfo {
-
- /**
- * Gets a copy of the set of local app apk file(s) and their versions. The returned order
- * matches the order in which the apks were added to the {@code IAppBuildInfo}.
- */
- public List<VersionedFile> getAppPackageFiles();
-
- /**
- * Adds the local apk file and its associated version. Note that apks will be returned from
- * {@link #getAppPackageFiles()} in the order in which they were added by this method.
- */
- public void addAppPackageFile(File appPackageFile, String version);
-
-}
+@Deprecated
+public interface IAppBuildInfo extends IBuildInfo {}
diff --git a/src/com/android/tradefed/build/IFileDownloader.java b/src/com/android/tradefed/build/IFileDownloader.java
index 3b3543f..b3196dd 100644
--- a/src/com/android/tradefed/build/IFileDownloader.java
+++ b/src/com/android/tradefed/build/IFileDownloader.java
@@ -16,6 +16,8 @@
package com.android.tradefed.build;
import java.io.File;
+import java.io.IOException;
+import java.util.List;
/**
* Interface for downloading a remote file.
@@ -57,4 +59,26 @@
throws BuildRetrievalError {
return true;
}
+
+ /**
+ * Download the files matching given filters in a remote zip file.
+ *
+ * <p>A file inside the remote zip file is only downloaded to its path matches any of the
+ * include filters but not the exclude filters.
+ *
+ * @param destDir the file to place the downloaded contents into.
+ * @param remoteFilePath the remote path to the file to download, relative to an implementation
+ * specific root.
+ * @param includeFilters a list of filters to download matching files.
+ * @param excludeFilters a list of filters to skip downloading matching files.
+ * @throws BuildRetrievalError if files could not be downloaded.
+ */
+ public default void downloadPartialFiles(
+ File destDir,
+ String remoteFilePath,
+ List<String> includeFilters,
+ List<String> excludeFilters)
+ throws BuildRetrievalError, IOException {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/src/com/android/tradefed/build/LocalAppBuildProvider.java b/src/com/android/tradefed/build/LocalAppBuildProvider.java
index 9219be3..a4ba19b 100644
--- a/src/com/android/tradefed/build/LocalAppBuildProvider.java
+++ b/src/com/android/tradefed/build/LocalAppBuildProvider.java
@@ -23,10 +23,7 @@
import java.util.ArrayList;
import java.util.Collection;
-/**
- * A {@link IBuildProvider} that constructs a {@link IAppBuildInfo} based on a provided local
- * path
- */
+/** A {@link IBuildProvider} that constructs a {@link IBuildInfo} based on a provided local path */
@OptionClass(alias = "local-app")
public class LocalAppBuildProvider extends StubBuildProvider {
@@ -45,16 +42,15 @@
// utilize parent build provider to set build id, test target name etc attributes if
// desired
IBuildInfo parentBuild = super.getBuild();
- IAppBuildInfo appBuild = new AppBuildInfo((BuildInfo)parentBuild);
for (File apkPath : mApkPaths) {
if (!apkPath.exists()) {
throw new IllegalArgumentException(String.format("path '%s' does not exist. "
+ "Please provide a valid file via --%s", apkPath.getAbsolutePath(),
APP_OPTION_NAME));
}
- appBuild.addAppPackageFile(apkPath, parentBuild.getBuildId());
+ parentBuild.addAppPackageFile(apkPath, parentBuild.getBuildId());
}
- return appBuild;
+ return parentBuild;
}
/**
diff --git a/src/com/android/tradefed/command/CommandOptions.java b/src/com/android/tradefed/command/CommandOptions.java
index d558e5f..64ef014 100644
--- a/src/com/android/tradefed/command/CommandOptions.java
+++ b/src/com/android/tradefed/command/CommandOptions.java
@@ -18,10 +18,11 @@
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
-import com.android.tradefed.device.metric.AutoLogCollector;
import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.config.OptionUpdateRule;
+import com.android.tradefed.device.metric.AutoLogCollector;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.retry.RetryStrategy;
import com.android.tradefed.util.UniqueMultiMap;
import java.util.LinkedHashSet;
@@ -122,14 +123,6 @@
private boolean mTokenSharding = false;
@Option(
- name = "skip-pre-device-setup",
- description =
- "allow TestInvocation to skip calling device.preInvocationSetup. This is for "
- + "delaying device setup when the test runs with VersionedTfLauncher."
- )
- private boolean mSkipPreDeviceSetup = false;
-
- @Option(
name = "dynamic-sharding",
description =
"Allow to dynamically move IRemoteTest from one shard to another. Only for local "
@@ -177,6 +170,12 @@
private boolean mUseParallelRemoteSetup = false;
@Option(
+ name = "report-module-progression",
+ description = "For remote invocation, whether or not to report progress at module level."
+ )
+ private boolean mReportModuleProgression = false;
+
+ @Option(
name = "auto-collect",
description =
"Specify a set of collectors that will be automatically managed by the harness "
@@ -201,6 +200,30 @@
)
private String mHostLogSuffix = null;
+ // [Options related to auto-retry]
+ @Option(
+ name = "max-testcase-run-count",
+ description =
+ "If the IRemoteTest can have its testcases run multiple times, "
+ + "the max number of runs for each testcase."
+ )
+ private int mMaxRunLimit = 1;
+
+ @Option(
+ name = "retry-strategy",
+ description =
+ "The retry strategy to be used when re-running some tests with "
+ + "--max-testcase-run-count"
+ )
+ private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
+
+ @Option(
+ name = "auto-retry",
+ description =
+ "Whether or not to enable the new auto-retry. This is a feature flag for testing."
+ )
+ private boolean mEnableAutoRetry = false;
+
/**
* Set the help mode for the config.
* <p/>
@@ -448,13 +471,6 @@
/** {@inheritDoc} */
@Override
-
- public boolean shouldSkipPreDeviceSetup() {
- return mSkipPreDeviceSetup;
- }
-
- /** {@inheritDoc} */
- @Override
public boolean shouldUseDynamicSharding() {
return mDynamicSharding;
}
@@ -536,4 +552,34 @@
public boolean shouldUseParallelRemoteSetup() {
return mUseParallelRemoteSetup;
}
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean shouldReportModuleProgression() {
+ return mReportModuleProgression;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int getMaxRetryCount() {
+ return mMaxRunLimit;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setMaxRetryCount(int maxRetryCount) {
+ mMaxRunLimit = maxRetryCount;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RetryStrategy getRetryStrategy() {
+ return mRetryStrategy;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean isAutoRetryEnabled() {
+ return mEnableAutoRetry;
+ }
}
diff --git a/src/com/android/tradefed/command/CommandRunner.java b/src/com/android/tradefed/command/CommandRunner.java
index e1bfb1f..4abd3d9 100644
--- a/src/com/android/tradefed/command/CommandRunner.java
+++ b/src/com/android/tradefed/command/CommandRunner.java
@@ -16,6 +16,8 @@
package com.android.tradefed.command;
+import com.android.tradefed.clearcut.ClearcutClient;
+import com.android.tradefed.clearcut.TerminateClearcutClient;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.GlobalConfiguration;
import com.android.tradefed.device.NoDeviceException;
@@ -77,7 +79,13 @@
public void run(String[] args) {
try {
initGlobalConfig(args);
+
+ ClearcutClient client = new ClearcutClient();
+ Runtime.getRuntime().addShutdownHook(new TerminateClearcutClient(client));
+ client.notifyTradefedStartEvent();
+
mScheduler = getCommandScheduler();
+ mScheduler.setClearcutClient(client);
mScheduler.start();
mScheduler.addCommand(args);
} catch (ConfigurationException e) {
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 5a8458f..35c8228 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -19,6 +19,7 @@
import com.android.ddmlib.DdmPreferences;
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.clearcut.ClearcutClient;
import com.android.tradefed.command.CommandFileParser.CommandLine;
import com.android.tradefed.command.CommandFileWatcher.ICommandFileListener;
import com.android.tradefed.command.CommandRunner.ExitCode;
@@ -165,6 +166,9 @@
private ExitCode mLastInvocationExitCode = ExitCode.NO_ERROR;
private Throwable mLastInvocationThrowable = null;
+ /** Client to report metric data of the harness. */
+ private ClearcutClient mClient = null;
+
@Option(name = "reload-cmdfiles", description =
"Whether to enable the command file autoreload mechanism")
// FIXME: enable this to be enabled or disabled on a per-cmdfile basis
@@ -731,7 +735,7 @@
// state during the interruption we at least do minimal tear down of devices with
// their built-in clean up.
CLog.d("Attempting postInvocationTearDown in stopInvocation");
- device.postInvocationTearDown();
+ device.postInvocationTearDown(null);
}
// If invocation is not currently in an interruptible state we provide a timer
// after which it will become interruptible.
@@ -997,6 +1001,9 @@
exit(manager);
cleanUp();
CLog.logAndDisplay(LogLevel.INFO, "All done");
+ if (mClient != null) {
+ mClient.stop();
+ }
} finally {
// Make sure that we don't quit with messages still in the buffers
System.err.flush();
@@ -1204,7 +1211,7 @@
CLog.logAndDisplay(LogLevel.ERROR, "Failed to get json command usage: %s", e);
}
} else if (config.getCommandOptions().isDryRunMode()) {
- config.validateOptions(false);
+ config.validateOptions();
String cmdLine = QuotationAwareTokenizer.combineTokens(args);
CLog.d("Dry run mode; skipping adding command: %s", cmdLine);
if (config.getCommandOptions().isNoisyDryRunMode()) {
@@ -2247,4 +2254,9 @@
public synchronized int getReadyCommandCount() {
return mReadyCommands.size();
}
+
+ @Override
+ public void setClearcutClient(ClearcutClient client) {
+ mClient = client;
+ }
}
diff --git a/src/com/android/tradefed/command/Console.java b/src/com/android/tradefed/command/Console.java
index a908246..49055c3 100644
--- a/src/com/android/tradefed/command/Console.java
+++ b/src/com/android/tradefed/command/Console.java
@@ -17,6 +17,8 @@
package com.android.tradefed.command;
import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.clearcut.ClearcutClient;
+import com.android.tradefed.clearcut.TerminateClearcutClient;
import com.android.tradefed.config.ArgsOptionParser;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
@@ -1143,12 +1145,19 @@
*/
public static void startConsole(Console console, String[] args) throws InterruptedException,
ConfigurationException {
+ ClearcutClient client = new ClearcutClient();
+ Runtime.getRuntime().addShutdownHook(new TerminateClearcutClient(client));
+ client.notifyTradefedStartEvent();
+
List<String> nonGlobalArgs = GlobalConfiguration.createGlobalConfiguration(args);
GlobalConfiguration.getInstance().setup();
console.setArgs(nonGlobalArgs);
console.setCommandScheduler(GlobalConfiguration.getInstance().getCommandScheduler());
console.setKeyStoreFactory(GlobalConfiguration.getInstance().getKeyStoreFactory());
console.setDaemon(true);
+
+ GlobalConfiguration.getInstance().getCommandScheduler().setClearcutClient(client);
+
console.start();
// Wait for the CommandScheduler to get started before we exit the main thread. See full
diff --git a/src/com/android/tradefed/command/ICommandOptions.java b/src/com/android/tradefed/command/ICommandOptions.java
index b2fefd2..9e5061d 100644
--- a/src/com/android/tradefed/command/ICommandOptions.java
+++ b/src/com/android/tradefed/command/ICommandOptions.java
@@ -17,6 +17,7 @@
package com.android.tradefed.command;
import com.android.tradefed.device.metric.AutoLogCollector;
+import com.android.tradefed.testtype.retry.RetryStrategy;
import com.android.tradefed.util.UniqueMultiMap;
import java.util.Set;
@@ -155,9 +156,6 @@
/** Whether or not sharding should use the token support. */
public boolean shouldUseTokenSharding();
- /** Return true if the test should skip device setup during TestInvocation setup. */
- public boolean shouldSkipPreDeviceSetup();
-
/** Returns if we should use dynamic sharding or not */
public boolean shouldUseDynamicSharding();
@@ -199,4 +197,19 @@
/** Whether or not to attempt parallel setup of the remote devices. */
public boolean shouldUseParallelRemoteSetup();
+
+ /** Whether or not to report progression of remote invocation at module level. */
+ public boolean shouldReportModuleProgression();
+
+ /** The maximum number of attempts during auto-retry. */
+ public int getMaxRetryCount();
+
+ /** Set the max retry count allowed during auto-retry. */
+ public void setMaxRetryCount(int maxRetryCount);
+
+ /** The {@link RetryStrategy} used during auto-retry. */
+ public RetryStrategy getRetryStrategy();
+
+ /** Whether or not to enable auto-retry. */
+ public boolean isAutoRetryEnabled();
}
diff --git a/src/com/android/tradefed/command/ICommandScheduler.java b/src/com/android/tradefed/command/ICommandScheduler.java
index aec4f8b..7a3ea3f 100644
--- a/src/com/android/tradefed/command/ICommandScheduler.java
+++ b/src/com/android/tradefed/command/ICommandScheduler.java
@@ -16,6 +16,7 @@
package com.android.tradefed.command;
+import com.android.tradefed.clearcut.ClearcutClient;
import com.android.tradefed.command.CommandRunner.ExitCode;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.IConfigurationFactory;
@@ -311,4 +312,7 @@
/** Returns the number of Commands in ready state in the queue. */
public int getReadyCommandCount();
+
+ /** Set the client to report harness data */
+ public void setClearcutClient(ClearcutClient client);
}
diff --git a/src/com/android/tradefed/command/Verify.java b/src/com/android/tradefed/command/Verify.java
deleted file mode 100644
index b72e8c0..0000000
--- a/src/com/android/tradefed/command/Verify.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.command;
-
-import com.android.tradefed.config.ArgsOptionParser;
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.Option;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Alternate Trade Federation entrypoint to validate command files
- */
-public class Verify {
-
- private static final int EXIT_STATUS_OKAY = 0x0;
- private static final int EXIT_STATUS_FAILED = 0x1;
-
- @Option(name = "cmdfile", description = "command file to verify")
- private List<File> mCmdFiles = new ArrayList<>();
-
- @Option(name = "show-commands", description = "Whether to print all generated commands")
- private boolean mShowCommands = false;
-
- @Option(name = "quiet", shortName = 'q', description = "Whether to silence all output. " +
- "Overrides all other output-related settings.")
- private boolean mQuiet = false;
-
- @Option(name = "help", shortName = 'h', description = "Print help")
- private boolean mHelp = false;
-
- /**
- * Returns whether the "--help"/"-h" option was passed to the instance
- */
- public boolean isHelpMode() {
- return mHelp;
- }
-
- /**
- * Program main entrypoint
- */
- public static void main(final String[] mainArgs) throws ConfigurationException {
- try {
- Verify verify = new Verify();
- ArgsOptionParser optionSetter = new ArgsOptionParser(verify);
- optionSetter.parse(mainArgs);
- if (verify.isHelpMode()) {
- // Print help, then exit
- System.err.println(ArgsOptionParser.getOptionHelp(false, verify));
- System.exit(EXIT_STATUS_OKAY);
- }
-
- if (verify.run()) {
- // true == everything's good
- System.exit(EXIT_STATUS_OKAY);
- } else {
- // false == whoopsie!
- System.exit(EXIT_STATUS_FAILED);
- }
-
- } finally {
- System.err.flush();
- System.out.flush();
- }
- }
-
- /**
- * Start validating all specified cmdfiles
- */
- public boolean run() {
- boolean anyFailures = false;
-
- for (File cmdFile : mCmdFiles) {
- try {
- // if verify returns false, then we set anyFailures to true
- anyFailures |= !runVerify(cmdFile);
-
- } catch (Throwable t) {
- if (!mQuiet) {
- System.err.format("Caught exception while parsing \"%s\"\n", cmdFile);
- System.err.println(t);
- }
- anyFailures = true;
- }
- }
-
- return !anyFailures;
- }
-
- /**
- * Validate the specified cmdfile
- */
- public boolean runVerify(File cmdFile) {
- final CommandFileParser parser = new CommandFileParser();
- try {
- List<CommandFileParser.CommandLine> commands = parser.parseFile(cmdFile);
- if (!mQuiet) {
- System.out.format("Successfully parsed %d commands from cmdfile %s\n",
- commands.size(), cmdFile);
-
- if (mShowCommands) {
- int i = 1;
- int digits = (int) Math.ceil(Math.log10(commands.size()));
- // Create a format string that will leave enough space for an index prefix
- // without mucking up alignment
- String format = String.format("%%%dd: %%s\n", digits);
- for (CommandFileParser.CommandLine cmd : commands) {
- System.out.format(format, i++, cmd);
- }
- }
- System.out.println();
- }
- } catch (ConfigurationException | IOException e) {
- if (!mQuiet) {
- System.err.format("Failed to parse %s:\n", cmdFile);
- System.err.println(e);
- }
-
- return false;
- }
-
- return true;
- }
-}
diff --git a/src/com/android/tradefed/config/Configuration.java b/src/com/android/tradefed/config/Configuration.java
index 9982d59..242c566 100644
--- a/src/com/android/tradefed/config/Configuration.java
+++ b/src/com/android/tradefed/config/Configuration.java
@@ -19,7 +19,6 @@
import com.android.tradefed.build.IBuildProvider;
import com.android.tradefed.command.CommandOptions;
import com.android.tradefed.command.ICommandOptions;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
import com.android.tradefed.config.OptionSetter.FieldDef;
import com.android.tradefed.device.IDeviceRecovery;
import com.android.tradefed.device.IDeviceSelection;
@@ -42,6 +41,7 @@
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.StubTest;
import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IDisableable;
import com.android.tradefed.util.MultiMap;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.keystore.IKeyStoreClient;
@@ -518,23 +518,34 @@
* Return a copy of all config objects
*/
private Collection<Object> getAllConfigurationObjects() {
- return getAllConfigurationObjects(null);
+ return getAllConfigurationObjects(null, true);
+ }
+
+ /** Return a copy of all config objects that are not disabled via {@link IDisableable}. */
+ private Collection<Object> getAllNonDisabledConfigurationObjects() {
+ return getAllConfigurationObjects(null, false);
}
/**
* Return a copy of all config objects, minus the object configuration of the type specified.
* Returns all the config objects if param is null.
*/
- private Collection<Object> getAllConfigurationObjects(String excludedConfigName) {
+ private Collection<Object> getAllConfigurationObjects(
+ String excludedConfigName, boolean includeDisabled) {
Collection<Object> objectsCopy = new ArrayList<Object>();
for (Entry<String, List<Object>> entryList : mConfigMap.entrySet()) {
- if (excludedConfigName != null) {
- // Only add if not a descriptor config object type.
- if (!excludedConfigName.equals(entryList.getKey())) {
- objectsCopy.addAll(entryList.getValue());
- }
- } else {
+ if (excludedConfigName != null && excludedConfigName.equals(entryList.getKey())) {
+ continue;
+ }
+ if (includeDisabled) {
objectsCopy.addAll(entryList.getValue());
+ } else {
+ for (Object o : entryList.getValue()) {
+ if (o instanceof IDisableable && ((IDisableable) o).isDisabled()) {
+ continue;
+ }
+ objectsCopy.add(o);
+ }
}
}
return objectsCopy;
@@ -1029,7 +1040,7 @@
// allow passing its option via command line.
ArgsOptionParser parser =
new ArgsOptionParser(
- getAllConfigurationObjects(CONFIGURATION_DESCRIPTION_TYPE_NAME));
+ getAllConfigurationObjects(CONFIGURATION_DESCRIPTION_TYPE_NAME, true));
if (keyStoreClient != null) {
parser.setKeyStore(keyStoreClient);
}
@@ -1272,13 +1283,7 @@
*/
@Override
public void validateOptions() throws ConfigurationException {
- validateOptions(true);
- }
-
- /** {@inheritDoc} */
- @Override
- public void validateOptions(boolean download) throws ConfigurationException {
- ArgsOptionParser argsParser = new ArgsOptionParser(getAllConfigurationObjects());
+ ArgsOptionParser argsParser = new ArgsOptionParser(getAllNonDisabledConfigurationObjects());
argsParser.validateMandatoryOptions();
ICommandOptions options = getCommandOptions();
if (options.getShardCount() != null && options.getShardCount() < 1) {
@@ -1289,16 +1294,21 @@
|| options.getShardIndex() >= options.getShardCount())) {
throw new ConfigurationException("a shard index must be in range [0, shard count)");
}
- // Parent invocation for local sharding should not resolved the dynamic @option yet.
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void resolveDynamicOptions() throws ConfigurationException {
+ ICommandOptions options = getCommandOptions();
if (options.getShardCount() != null && options.getShardIndex() == null) {
- download = false;
CLog.w("Skipping download due to local sharding detected.");
+ return;
}
- if (download) {
- CLog.d("Resolve and download remote files from @Option");
- // Setup and validate the GCS File paths
- mRemoteFiles.addAll(argsParser.validateRemoteFilePath());
- }
+
+ ArgsOptionParser argsParser = new ArgsOptionParser(getAllConfigurationObjects());
+ CLog.d("Resolve and download remote files from @Option");
+ // Setup and validate the GCS File paths
+ mRemoteFiles.addAll(argsParser.validateRemoteFilePath());
}
/** {@inheritDoc} */
@@ -1320,13 +1330,16 @@
/** {@inheritDoc} */
@Override
public void dumpXml(PrintWriter output, List<String> excludeFilters) throws IOException {
- dumpXml(output, excludeFilters, true);
+ dumpXml(output, excludeFilters, true, true);
}
/** {@inheritDoc} */
@Override
public void dumpXml(
- PrintWriter output, List<String> excludeFilters, boolean printDeprecatedOptions)
+ PrintWriter output,
+ List<String> excludeFilters,
+ boolean printDeprecatedOptions,
+ boolean printUnchangedOptions)
throws IOException {
KXmlSerializer serializer = new KXmlSerializer();
serializer.setOutput(output);
@@ -1340,7 +1353,8 @@
MULTI_PRE_TARGET_PREPARER_TYPE_NAME,
multiPreTargerPrep,
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
for (IMultiTargetPreparer multipreparer : getMultiTargetPreparers()) {
@@ -1349,7 +1363,8 @@
MULTI_PREPARER_TYPE_NAME,
multipreparer,
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
if (getDeviceConfig().size() > 1) {
@@ -1365,33 +1380,38 @@
BUILD_PROVIDER_TYPE_NAME,
deviceConfig.getBuildProvider(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
for (ITargetPreparer preparer : deviceConfig.getTargetPreparers()) {
ConfigurationUtil.dumpClassToXml(
serializer,
TARGET_PREPARER_TYPE_NAME,
preparer,
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
ConfigurationUtil.dumpClassToXml(
serializer,
DEVICE_RECOVERY_TYPE_NAME,
deviceConfig.getDeviceRecovery(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
ConfigurationUtil.dumpClassToXml(
serializer,
DEVICE_REQUIREMENTS_TYPE_NAME,
deviceConfig.getDeviceRequirements(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
ConfigurationUtil.dumpClassToXml(
serializer,
DEVICE_OPTIONS_TYPE_NAME,
deviceConfig.getDeviceOptions(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
serializer.endTag(null, Configuration.DEVICE_NAME);
}
} else {
@@ -1401,70 +1421,85 @@
BUILD_PROVIDER_TYPE_NAME,
getBuildProvider(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
for (ITargetPreparer preparer : getTargetPreparers()) {
ConfigurationUtil.dumpClassToXml(
serializer,
TARGET_PREPARER_TYPE_NAME,
preparer,
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
ConfigurationUtil.dumpClassToXml(
serializer,
DEVICE_RECOVERY_TYPE_NAME,
getDeviceRecovery(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
ConfigurationUtil.dumpClassToXml(
serializer,
DEVICE_REQUIREMENTS_TYPE_NAME,
getDeviceRequirements(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
ConfigurationUtil.dumpClassToXml(
serializer,
DEVICE_OPTIONS_TYPE_NAME,
getDeviceOptions(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
for (IRemoteTest test : getTests()) {
ConfigurationUtil.dumpClassToXml(
- serializer, TEST_TYPE_NAME, test, excludeFilters, printDeprecatedOptions);
+ serializer,
+ TEST_TYPE_NAME,
+ test,
+ excludeFilters,
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
ConfigurationUtil.dumpClassToXml(
serializer,
CONFIGURATION_DESCRIPTION_TYPE_NAME,
getConfigurationDescription(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
ConfigurationUtil.dumpClassToXml(
serializer,
LOGGER_TYPE_NAME,
getLogOutput(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
ConfigurationUtil.dumpClassToXml(
serializer,
LOG_SAVER_TYPE_NAME,
getLogSaver(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
for (ITestInvocationListener listener : getTestInvocationListeners()) {
ConfigurationUtil.dumpClassToXml(
serializer,
RESULT_REPORTER_TYPE_NAME,
listener,
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
ConfigurationUtil.dumpClassToXml(
serializer,
CMD_OPTIONS_TYPE_NAME,
getCommandOptions(),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
for (IMetricCollector collector : getMetricCollectors()) {
ConfigurationUtil.dumpClassToXml(
@@ -1472,7 +1507,8 @@
DEVICE_METRICS_COLLECTOR_TYPE_NAME,
collector,
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
for (ISystemStatusChecker checker : getSystemStatusCheckers()) {
@@ -1481,7 +1517,8 @@
SYSTEM_STATUS_CHECKER_TYPE_NAME,
checker,
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
ConfigurationUtil.dumpClassToXml(
@@ -1489,7 +1526,8 @@
SANBOX_OPTIONS_TYPE_NAME,
getConfigurationObject(SANBOX_OPTIONS_TYPE_NAME),
excludeFilters,
- printDeprecatedOptions);
+ printDeprecatedOptions,
+ printUnchangedOptions);
serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
serializer.endDocument();
diff --git a/src/com/android/tradefed/config/ConfigurationDef.java b/src/com/android/tradefed/config/ConfigurationDef.java
index c99b79f..4024356 100644
--- a/src/com/android/tradefed/config/ConfigurationDef.java
+++ b/src/com/android/tradefed/config/ConfigurationDef.java
@@ -16,12 +16,10 @@
package com.android.tradefed.config;
-import com.android.tradefed.build.BuildSerializedVersion;
import com.android.tradefed.device.metric.IMetricCollector;
import com.android.tradefed.log.LogUtil.CLog;
import java.io.File;
-import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -51,38 +49,6 @@
/** The set of files (and modification times) that were used to load this config */
private final Map<File, Long> mSourceFiles = new HashMap<>();
- /** Holds the details of an option. */
- public static final class OptionDef implements Serializable {
- private static final long serialVersionUID = BuildSerializedVersion.VERSION;
-
- public final String name;
- public final String key;
- public final String value;
- public final String source;
- public final String applicableObjectType;
-
- public OptionDef(String optionName, String optionValue, String source) {
- this(optionName, null, optionValue, source, null);
- }
-
- public OptionDef(String optionName, String optionKey, String optionValue, String source) {
- this(optionName, optionKey, optionValue, source, null);
- }
-
- public OptionDef(
- String optionName,
- String optionKey,
- String optionValue,
- String source,
- String type) {
- this.name = optionName;
- this.key = optionKey;
- this.value = optionValue;
- this.source = source;
- this.applicableObjectType = type;
- }
- }
-
/**
* Object to hold info for a className and the appearance number it has (e.g. if a config has
* the same object twice, the first one will have the first appearance number).
diff --git a/src/com/android/tradefed/config/ConfigurationUtil.java b/src/com/android/tradefed/config/ConfigurationUtil.java
index f162181..e1c539b 100644
--- a/src/com/android/tradefed/config/ConfigurationUtil.java
+++ b/src/com/android/tradefed/config/ConfigurationUtil.java
@@ -72,16 +72,24 @@
* be excluded from the dump. for example: {@link Configuration#TARGET_PREPARER_TYPE_NAME}.
* com.android.tradefed.testtype.StubTest
* @param printDeprecatedOptions whether or not to print deprecated options
+ * @param printUnchangedOptions whether or not to print options that haven't been changed
*/
static void dumpClassToXml(
KXmlSerializer serializer,
String classTypeName,
Object obj,
List<String> excludeClassFilter,
- boolean printDeprecatedOptions)
+ boolean printDeprecatedOptions,
+ boolean printUnchangedOptions)
throws IOException {
dumpClassToXml(
- serializer, classTypeName, obj, false, excludeClassFilter, printDeprecatedOptions);
+ serializer,
+ classTypeName,
+ obj,
+ false,
+ excludeClassFilter,
+ printDeprecatedOptions,
+ printUnchangedOptions);
}
/**
@@ -95,6 +103,7 @@
* be excluded from the dump. for example: {@link Configuration#TARGET_PREPARER_TYPE_NAME}.
* com.android.tradefed.testtype.StubTest
* @param printDeprecatedOptions whether or not to print deprecated options
+ * @param printUnchangedOptions whether or not to print options that haven't been changed
*/
static void dumpClassToXml(
KXmlSerializer serializer,
@@ -102,7 +111,8 @@
Object obj,
boolean isGenericObject,
List<String> excludeClassFilter,
- boolean printDeprecatedOptions)
+ boolean printDeprecatedOptions,
+ boolean printUnchangedOptions)
throws IOException {
if (excludeClassFilter.contains(classTypeName)) {
return;
@@ -114,12 +124,12 @@
serializer.startTag(null, "object");
serializer.attribute(null, "type", classTypeName);
serializer.attribute(null, CLASS_NAME, obj.getClass().getName());
- dumpOptionsToXml(serializer, obj, printDeprecatedOptions);
+ dumpOptionsToXml(serializer, obj, printDeprecatedOptions, printUnchangedOptions);
serializer.endTag(null, "object");
} else {
serializer.startTag(null, classTypeName);
serializer.attribute(null, CLASS_NAME, obj.getClass().getName());
- dumpOptionsToXml(serializer, obj, printDeprecatedOptions);
+ dumpOptionsToXml(serializer, obj, printDeprecatedOptions, printUnchangedOptions);
serializer.endTag(null, classTypeName);
}
}
@@ -130,13 +140,20 @@
* @param serializer a {@link KXmlSerializer} to create the XML dump
* @param obj {@link Object} to be added to the XML dump
* @param printDeprecatedOptions whether or not to skip the deprecated options
+ * @param printUnchangedOptions whether or not to print options that haven't been changed
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static void dumpOptionsToXml(
- KXmlSerializer serializer, Object obj, boolean printDeprecatedOptions)
+ KXmlSerializer serializer,
+ Object obj,
+ boolean printDeprecatedOptions,
+ boolean printUnchangedOptions)
throws IOException {
for (Field field : OptionSetter.getOptionFieldsForClass(obj.getClass())) {
Option option = field.getAnnotation(Option.class);
+ if (!printUnchangedOptions && !option.isChanged()) {
+ continue;
+ }
Deprecated deprecatedAnnotation = field.getAnnotation(Deprecated.class);
// If enabled, skip @Deprecated options
if (!printDeprecatedOptions && deprecatedAnnotation != null) {
diff --git a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
index b25f556..2585069 100644
--- a/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/DynamicRemoteFileResolver.java
@@ -18,13 +18,21 @@
import com.android.annotations.VisibleForTesting;
import com.android.tradefed.config.OptionSetter.OptionFieldsForName;
import com.android.tradefed.config.remote.GcsRemoteFileResolver;
+import com.android.tradefed.config.remote.HttpRemoteFileResolver;
+import com.android.tradefed.config.remote.HttpsRemoteFileResolver;
import com.android.tradefed.config.remote.IRemoteFileResolver;
+import com.android.tradefed.config.remote.LocalFileResolver;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.MultiMap;
+import com.android.tradefed.util.ZipUtil;
+import com.android.tradefed.util.ZipUtil2;
import java.io.File;
+import java.io.IOException;
import java.lang.reflect.Field;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -49,9 +57,16 @@
static {
PROTOCOL_SUPPORT.put(GcsRemoteFileResolver.PROTOCOL, new GcsRemoteFileResolver());
+ PROTOCOL_SUPPORT.put(LocalFileResolver.PROTOCOL, new LocalFileResolver());
+ PROTOCOL_SUPPORT.put(HttpRemoteFileResolver.PROTOCOL_HTTP, new HttpRemoteFileResolver());
+ PROTOCOL_SUPPORT.put(HttpsRemoteFileResolver.PROTOCOL_HTTPS, new HttpsRemoteFileResolver());
}
// The configuration map being static, we only need to update it once per TF instance.
private static AtomicBoolean sIsUpdateDone = new AtomicBoolean(false);
+ // Query key for requesting to unzip a downloaded file automatically.
+ public static final String UNZIP_KEY = "unzip";
+ // Query key for requesting a download to be optional, so if it fails we don't replace it.
+ public static final String OPTIONAL_KEY = "optional";
private Map<String, OptionFieldsForName> mOptionMap;
@@ -69,9 +84,11 @@
public final Set<File> validateRemoteFilePath() throws ConfigurationException {
Set<File> downloadedFiles = new HashSet<>();
try {
+ Map<Object, Field> fieldSeen = new HashMap<>();
for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
final OptionFieldsForName optionFields = optionPair.getValue();
for (Map.Entry<Object, Field> fieldEntry : optionFields) {
+
final Object obj = fieldEntry.getKey();
final Field field = fieldEntry.getValue();
final Option option = field.getAnnotation(Option.class);
@@ -83,14 +100,21 @@
final Object value;
try {
value = field.get(obj);
+ if (value == null) {
+ continue;
+ }
} catch (IllegalAccessException e) {
throw new ConfigurationException(
String.format("internal error: %s", e.getMessage()));
}
- if (value == null) {
+ if (fieldSeen.get(value) != null && fieldSeen.get(value).equals(field)) {
continue;
- } else if (value instanceof File) {
+ }
+ // Keep track of the field set on each object
+ fieldSeen.put(value, field);
+
+ if (value instanceof File) {
File consideredFile = (File) value;
File downloadedFile = resolveRemoteFiles(consideredFile, option);
if (downloadedFile != null) {
@@ -188,6 +212,62 @@
return downloadedFiles;
}
+ /**
+ * Download the files matching given filters in a remote zip file.
+ *
+ * <p>A file inside the remote zip file is only downloaded if its path matches any of the
+ * include filters but not the exclude filters.
+ *
+ * @param destDir the file to place the downloaded contents into.
+ * @param remoteZipFilePath the remote path to the zip file to download, relative to an
+ * implementation specific root.
+ * @param includeFilters a list of regex strings to download matching files. A file's path
+ * matching any filter will be downloaded.
+ * @param excludeFilters a list of regex strings to skip downloading matching files. A file's
+ * path matching any filter will not be downloaded.
+ * @throws ConfigurationException if files could not be downloaded.
+ */
+ public void resolvePartialDownloadZip(
+ File destDir,
+ String remoteZipFilePath,
+ List<String> includeFilters,
+ List<String> excludeFilters)
+ throws ConfigurationException {
+ Map<String, String> queryArgs;
+ String protocol;
+ try {
+ URI uri = new URI(remoteZipFilePath);
+ protocol = uri.getScheme();
+ queryArgs = parseQuery(uri.getQuery());
+ } catch (URISyntaxException e) {
+ throw new ConfigurationException(
+ String.format(
+ "Failed to parse the remote zip file path: %s", remoteZipFilePath),
+ e);
+ }
+ IRemoteFileResolver resolver = getResolver(protocol);
+
+ queryArgs.put("partial_download_dir", destDir.getAbsolutePath());
+ if (includeFilters != null) {
+ queryArgs.put("include_filters", String.join(";", includeFilters));
+ }
+ if (excludeFilters != null) {
+ queryArgs.put("exclude_filters", String.join(";", excludeFilters));
+ }
+ // Downloaded individual files should be saved to destDir, return value is not needed.
+ try {
+ resolver.resolveRemoteFiles(new File(remoteZipFilePath), null, queryArgs);
+ } catch (ConfigurationException e) {
+ if (isOptional(queryArgs)) {
+ CLog.d(
+ "Failed to partially download '%s' but marked optional so skipping: %s",
+ remoteZipFilePath, e.getMessage());
+ } else {
+ throw e;
+ }
+ }
+ }
+
@VisibleForTesting
protected IRemoteFileResolver getResolver(String protocol) {
if (updateProtocols()) {
@@ -216,27 +296,83 @@
return GlobalConfiguration.getInstance();
}
+ /**
+ * Utility that allows to check whether or not a file should be unzip and unzip it if required.
+ */
+ public static final File unzipIfRequired(File downloadedFile, Map<String, String> query)
+ throws IOException {
+ String unzipValue = query.get(UNZIP_KEY);
+ if (unzipValue != null && "true".equals(unzipValue.toLowerCase())) {
+ // File was requested to be unzipped.
+ if (ZipUtil.isZipFileValid(downloadedFile, false)) {
+ File unzipped =
+ ZipUtil2.extractZipToTemp(
+ downloadedFile, FileUtil.getBaseName(downloadedFile.getName()));
+ FileUtil.deleteFile(downloadedFile);
+ return unzipped;
+ } else {
+ CLog.w("%s was requested to be unzipped but is not a valid zip.", downloadedFile);
+ }
+ }
+ // Return the original file untouched
+ return downloadedFile;
+ }
+
private File resolveRemoteFiles(File consideredFile, Option option)
throws ConfigurationException {
+ File fileToResolve;
String path = consideredFile.getPath();
- String protocol = getProtocol(path);
+ String protocol;
+ Map<String, String> query;
+ try {
+ URI uri = new URI(path);
+ protocol = uri.getScheme();
+ query = parseQuery(uri.getQuery());
+ fileToResolve = new File(protocol + ":" + uri.getPath());
+ } catch (URISyntaxException e) {
+ CLog.e(e);
+ return null;
+ }
IRemoteFileResolver resolver = getResolver(protocol);
if (resolver != null) {
- return resolver.resolveRemoteFiles(consideredFile, option);
+ try {
+ return resolver.resolveRemoteFiles(fileToResolve, option, query);
+ } catch (ConfigurationException e) {
+ if (isOptional(query)) {
+ CLog.d(
+ "Failed to resolve '%s' but marked optional so skipping: %s",
+ fileToResolve, e.getMessage());
+ } else {
+ throw e;
+ }
+ }
}
// Not a remote file
return null;
}
/**
- * Java URL doesn't recognize 'gs' as a protocol and throws an exception so we do the protocol
- * extraction ourselves.
+ * Parse a URL query style. Delimited by &, and map values represented by =. Example:
+ * ?key=value&key2=value2
*/
- private String getProtocol(String path) {
- int index = path.indexOf(":/");
- if (index == -1) {
- return "";
+ private Map<String, String> parseQuery(String query) {
+ Map<String, String> values = new HashMap<>();
+ if (query == null) {
+ return values;
}
- return path.substring(0, index);
+ for (String maps : query.split("&")) {
+ String[] keyVal = maps.split("=");
+ values.put(keyVal[0], keyVal[1]);
+ }
+ return values;
+ }
+
+ /** Whether or not a link was requested as optional. */
+ private boolean isOptional(Map<String, String> query) {
+ String value = query.get(OPTIONAL_KEY);
+ if (value == null) {
+ return false;
+ }
+ return "true".equals(value.toLowerCase());
}
}
diff --git a/src/com/android/tradefed/config/IConfiguration.java b/src/com/android/tradefed/config/IConfiguration.java
index bc8d1dd..53751cc 100644
--- a/src/com/android/tradefed/config/IConfiguration.java
+++ b/src/com/android/tradefed/config/IConfiguration.java
@@ -18,7 +18,6 @@
import com.android.tradefed.build.IBuildProvider;
import com.android.tradefed.command.ICommandOptions;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
import com.android.tradefed.device.IDeviceRecovery;
import com.android.tradefed.device.IDeviceSelection;
import com.android.tradefed.device.TestDeviceOptions;
@@ -37,6 +36,7 @@
import org.json.JSONArray;
import org.json.JSONException;
+import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
@@ -533,14 +533,12 @@
public void validateOptions() throws ConfigurationException;
/**
- * Validate option values.
+ * Resolve options of {@link File} pointing to a remote location. This requires {@link
+ * #cleanDynamicOptionFiles()} to be called to clean up the files.
*
- * <p>Currently this will just validate that all mandatory options have been set
- *
- * @param download Whether or not to download the files associated to a remote path
- * @throws ConfigurationException if config is not valid
+ * @throws ConfigurationException
*/
- public void validateOptions(boolean download) throws ConfigurationException;
+ public void resolveDynamicOptions() throws ConfigurationException;
/** Delete any files that was downloaded to resolved Option fields of remote files. */
public void cleanDynamicOptionFiles();
@@ -594,6 +592,9 @@
* @throws IOException
*/
public void dumpXml(
- PrintWriter output, List<String> excludeFilters, boolean printDeprecatedOptions)
+ PrintWriter output,
+ List<String> excludeFilters,
+ boolean printDeprecatedOptions,
+ boolean printUnchangedOptions)
throws IOException;
}
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index 9e28d8b..1697667 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -29,6 +29,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
@@ -533,10 +534,35 @@
"internal error when setting option '%s'", optionName), e);
}
-
+ Option option = field.getAnnotation(Option.class);
+ updateOptionChangedField(option);
return fieldWasSet;
}
+ /**
+ * Change the {@link Option#isChanged()} field to True if we are setting a value on that option.
+ * This will help to track which options are used.
+ */
+ @SuppressWarnings("unchecked")
+ private static void updateOptionChangedField(Option option) {
+ Object handler = Proxy.getInvocationHandler(option);
+ Field f;
+ try {
+ // "memberValues" is a special field for annotation and their method current values.
+ f = handler.getClass().getDeclaredField("memberValues");
+ } catch (NoSuchFieldException | SecurityException e) {
+ throw new IllegalStateException(e);
+ }
+ f.setAccessible(true);
+ Map<String, Object> memberValues;
+ try {
+ memberValues = (Map<String, Object>) f.get(handler);
+ // Set the #isChanged() method return to true.
+ memberValues.put("isChanged", true);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ throw new IllegalStateException(e);
+ }
+ }
/**
* Sets the given {@link Option} fields value.
diff --git a/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java b/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
index afaa5be..8d1ecbd 100644
--- a/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/remote/GcsRemoteFileResolver.java
@@ -19,10 +19,13 @@
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.gcs.GCSDownloaderHelper;
import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
import com.android.tradefed.config.Option;
import com.android.tradefed.log.LogUtil.CLog;
import java.io.File;
+import java.io.IOException;
+import java.util.Map;
import javax.annotation.Nonnull;
@@ -34,15 +37,17 @@
private GCSDownloaderHelper mHelper = null;
@Override
- public File resolveRemoteFiles(File consideredFile, Option option)
+ public File resolveRemoteFiles(File consideredFile, Option option, Map<String, String> query)
throws ConfigurationException {
// Don't use absolute path as it would not start with gs:
String path = consideredFile.getPath();
CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
- // We need to download the file from the bucket
try {
- return getDownloader().fetchTestResource(path);
- } catch (BuildRetrievalError e) {
+ // We need to download the file from the bucket
+ File downloadedFile = getDownloader().fetchTestResource(path);
+ // Unzip it if required
+ return DynamicRemoteFileResolver.unzipIfRequired(downloadedFile, query);
+ } catch (BuildRetrievalError | IOException e) {
CLog.e(e);
throw new ConfigurationException(
String.format("Failed to download %s due to: %s", path, e.getMessage()), e);
diff --git a/src/com/android/tradefed/config/remote/HttpRemoteFileResolver.java b/src/com/android/tradefed/config/remote/HttpRemoteFileResolver.java
new file mode 100644
index 0000000..b92c6c4
--- /dev/null
+++ b/src/com/android/tradefed/config/remote/HttpRemoteFileResolver.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.net.HttpHelper;
+import com.android.tradefed.util.net.IHttpHelper;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+/** Implementation of {@link IRemoteFileResolver} that allows downloading remote file via http */
+public class HttpRemoteFileResolver implements IRemoteFileResolver {
+
+ public static final String PROTOCOL_HTTP = "http";
+
+ @Override
+ public File resolveRemoteFiles(
+ File consideredFile, Option option, Map<String, String> queryArgs)
+ throws ConfigurationException {
+ // Don't use absolute path as it would not start with gs:
+ String path = consideredFile.getPath();
+ CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
+ // Replace the very first / by // to be http:// again.
+ path = path.replaceFirst(":/", "://");
+
+ IHttpHelper downloader = getDownloader();
+ File downloadedFile = null;
+ try {
+ downloadedFile =
+ FileUtil.createTempFile(
+ FileUtil.getBaseName(consideredFile.getName()),
+ FileUtil.getExtension(consideredFile.getName()));
+ downloader.doGet(path, new FileOutputStream(downloadedFile));
+ } catch (IOException | RuntimeException e) {
+ FileUtil.deleteFile(downloadedFile);
+ throw new ConfigurationException(
+ String.format("Failed to download %s due to: %s", path, e.getMessage()), e);
+ }
+ return downloadedFile;
+ }
+
+ @Override
+ public @Nonnull String getSupportedProtocol() {
+ return PROTOCOL_HTTP;
+ }
+
+ @VisibleForTesting
+ protected IHttpHelper getDownloader() {
+ return new HttpHelper();
+ }
+}
diff --git a/src/com/android/tradefed/config/remote/HttpsRemoteFileResolver.java b/src/com/android/tradefed/config/remote/HttpsRemoteFileResolver.java
new file mode 100644
index 0000000..acb5c03
--- /dev/null
+++ b/src/com/android/tradefed/config/remote/HttpsRemoteFileResolver.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import javax.annotation.Nonnull;
+
+/** Implementation of {@link IRemoteFileResolver} that allows downloading remote file via https */
+public class HttpsRemoteFileResolver extends HttpRemoteFileResolver {
+
+ public static final String PROTOCOL_HTTPS = "https";
+
+ @Override
+ public @Nonnull String getSupportedProtocol() {
+ return PROTOCOL_HTTPS;
+ }
+}
diff --git a/src/com/android/tradefed/config/remote/IRemoteFileResolver.java b/src/com/android/tradefed/config/remote/IRemoteFileResolver.java
index 3d54ecb..f1413bd 100644
--- a/src/com/android/tradefed/config/remote/IRemoteFileResolver.java
+++ b/src/com/android/tradefed/config/remote/IRemoteFileResolver.java
@@ -19,6 +19,7 @@
import com.android.tradefed.config.Option;
import java.io.File;
+import java.util.Map;
import javax.annotation.Nonnull;
@@ -36,8 +37,25 @@
* @return The resolved local file.
* @throws ConfigurationException if something goes wrong.
*/
- public @Nonnull File resolveRemoteFiles(File consideredFile, Option option)
- throws ConfigurationException;
+ public default @Nonnull File resolveRemoteFiles(File consideredFile, Option option)
+ throws ConfigurationException {
+ throw new ConfigurationException("Should not have been called");
+ }
+
+ /**
+ * Resolve the remote file.
+ *
+ * @param consideredFile {@link File} evaluated as remote.
+ * @param option The original option configuring the file.
+ * @param queryArgs The arguments passed as a query to the URL.
+ * @return The resolved local file.
+ * @throws ConfigurationException if something goes wrong.
+ */
+ public default @Nonnull File resolveRemoteFiles(
+ File consideredFile, Option option, Map<String, String> queryArgs)
+ throws ConfigurationException {
+ return resolveRemoteFiles(consideredFile, option);
+ }
/** Returns the associated protocol supported for download. */
public @Nonnull String getSupportedProtocol();
diff --git a/src/com/android/tradefed/config/remote/LocalFileResolver.java b/src/com/android/tradefed/config/remote/LocalFileResolver.java
new file mode 100644
index 0000000..432098f
--- /dev/null
+++ b/src/com/android/tradefed/config/remote/LocalFileResolver.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+
+import javax.annotation.Nonnull;
+
+/** Implementation of {@link IRemoteFileResolver} that allows linking local files */
+public class LocalFileResolver implements IRemoteFileResolver {
+
+ public static final String PROTOCOL = "file";
+
+ @Override
+ public File resolveRemoteFiles(File consideredFile, Option option)
+ throws ConfigurationException {
+ // Don't use absolute path as it would not start with gs:
+ String path = consideredFile.getPath();
+ CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
+ String pathWithoutProtocol = path.replaceFirst(PROTOCOL + ":", "");
+ File localFile = new File(pathWithoutProtocol);
+ if (localFile.exists()) {
+ return localFile;
+ }
+ throw new ConfigurationException(String.format("Failed to find local file %s.", localFile));
+ }
+
+ @Override
+ public @Nonnull String getSupportedProtocol() {
+ return PROTOCOL;
+ }
+}
diff --git a/src/com/android/tradefed/device/CpuStatsCollector.java b/src/com/android/tradefed/device/CpuStatsCollector.java
deleted file mode 100644
index 18695ce..0000000
--- a/src/com/android/tradefed/device/CpuStatsCollector.java
+++ /dev/null
@@ -1,501 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.ddmlib.MultiLineReceiver;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.SimpleStats;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Helper class which runs {@code cpustats} continuously on an {@link ITestDevice} and parses the
- * output.
- * <p>
- * Provides a method to record the output of {@code cpustats} and get all the cpu usage measurements
- * as well methods for performing calculations on that data to find the mean of the cpu workload,
- * the approximate cpu frequency, and the percentage of used cpu frequency.
- * </p><p>
- * This is meant to be a replacement for {@link TopHelper}, which does not provide stats about cpu
- * frequency and has a higher overhead due to measuring processes/threads.
- * </p><p>
- * The {@code cpustats} command was added in the Jellybean release, so this collector should only be
- * used for new tests.
- * </p>
- * @see TopHelper
- */
-public class CpuStatsCollector extends Thread {
- private static final String CPU_STATS_CMD = "cpustats -m -d %s";
-
- private final ITestDevice mTestDevice;
- private long mDelay;
-
- /**
- * Used to distinguish between the different CPU time categories.
- */
- enum TimeCategory {
- USER,
- NICE,
- SYS,
- IDLE,
- IOW,
- IRQ,
- SIRQ
- }
-
- /**
- * Class for holding parsed output data for a single {@code cpustats} output.
- * <p>
- * This class holds parsed output, and also performs simple calculations on that data. The
- * methods which perform these calucations should only be called after the object has been
- * populated.
- * </p>
- */
- public static class CpuStats {
- public Map<TimeCategory, Integer> mTimeStats = new HashMap<TimeCategory, Integer>();
- public Map<Integer, Integer> mFreqStats = new HashMap<Integer, Integer>();
- private Map<TimeCategory, Double> mPercentageStats = new HashMap<TimeCategory, Double>();
- private Integer mTotalTime = null;
- private Double mAverageMhz = null;
-
- /**
- * Get the percentage of cycles used on a given category.
- */
- public Double getPercentage(TimeCategory category) {
- if (!mPercentageStats.containsKey(category)) {
- mPercentageStats.put(category, 100.0 * mTimeStats.get(category) / getTotalTime());
- }
- return mPercentageStats.get(category);
- }
-
- /**
- * Estimate the MHz used by the cpu during the duration.
- * <p>
- * This is calculated by:
- * </p><code>
- * ((sum(c_time) - idle) / sum(c_time)) * (sum(freq * f_time) / sum(f_time))
- * </code><p>
- * where {@code c_time} is the time for a given category, {@code idle} is the time in the
- * idle state, {@code freq} is a frequency and {@code f_time} is the time spent in that
- * frequency.
- * </p>
- */
- public Double getEstimatedMhz() {
- if (mFreqStats.isEmpty()) {
- return null;
- }
- return getTotalUsage() * getAverageMhz();
- }
-
- /**
- * Get the amount of MHz as a percentage of available MHz used by the cpu during the
- * duration.
- * <p>
- * This is calculated by:
- * </p><code>
- * 100 * sum(freq * f_time) / (max_freq * sum(f_time))
- * </code><p>
- * where {@code freq} is a frequency, {@code f_time} is the time spent in that frequency,
- * and {@code max_freq} is the maximum frequency the cpu is capable of.
- * </p>
- */
- public Double getUsedMhzPercentage() {
- if (mFreqStats.isEmpty()) {
- return null;
- }
- return 100.0 * getAverageMhz() / getMaxMhz();
- }
-
- /**
- * Get the total usage, or the sum of all the times except idle over the sum of all the
- * times.
- */
- private Double getTotalUsage() {
- return (double) (getTotalTime() - mTimeStats.get(TimeCategory.IDLE)) / getTotalTime();
- }
-
- /**
- * Get the average MHz.
- * <p>
- * This is calculated by:
- * </p><code>
- * sum(freq * f_time) / sum(f_time))
- * </code><p>
- * where {@code freq} is a frequency and {@code f_time} is the time spent in that frequency.
- * </p>
- */
- private Double getAverageMhz() {
- if (mFreqStats.isEmpty()) {
- return null;
- }
- if (mAverageMhz == null) {
- double sumFreqTime = 0.0;
- long sumTime = 0;
- for (Map.Entry<Integer, Integer> e : mFreqStats.entrySet()) {
- sumFreqTime += e.getKey() * e.getValue() / 1000.0;
- sumTime += e.getValue();
- }
- mAverageMhz = sumFreqTime / sumTime;
- }
- return mAverageMhz;
- }
-
- /**
- * Get the maximum possible MHz.
- */
- private Double getMaxMhz() {
- if (mFreqStats.isEmpty()) {
- return null;
- }
- int max = 0;
- for (int freq : mFreqStats.keySet()) {
- max = Math.max(max, freq);
- }
- return max / 1000.0;
- }
-
- /**
- * Get the total amount of time cycles.
- */
- private Integer getTotalTime() {
- if (mTotalTime == null) {
- int sum = 0;
- for (int time : mTimeStats.values()) {
- sum += time;
- }
- mTotalTime = sum;
- }
- return mTotalTime;
- }
- }
-
- /**
- * Receiver which parses the output from {@code cpustats} and optionally logs to a file.
- */
- public static class CpuStatsReceiver extends MultiLineReceiver {
- private Map<String, List<CpuStats>> mCpuStats = new HashMap<String, List<CpuStats>>(4);
-
- private boolean mIsCancelled = false;
- private File mLogFile = null;
- private BufferedWriter mLogWriter = null;
-
- public CpuStatsReceiver() {
- setTrimLine(false);
- }
-
- /**
- * Specify a file to log the output to.
- * <p>
- * This can be called at any time in the receivers life cycle, but only new output will be
- * logged to the file.
- * </p>
- */
- public synchronized void logToFile(File logFile) {
- try {
- mLogFile = logFile;
- mLogWriter = new BufferedWriter(new FileWriter(mLogFile));
- } catch (IOException e) {
- CLog.e("IOException when creating a fileWriter:");
- CLog.e(e);
- mLogWriter = null;
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void processNewLines(String[] lines) {
- if (mIsCancelled) {
- return;
- }
- synchronized (this) {
- if (mLogWriter != null) {
- try {
- for (String line : lines) {
- mLogWriter.write(line + "\n");
- }
- } catch (IOException e) {
- CLog.e("Error writing to file");
- CLog.e(e);
- }
- }
- }
- for (String line : lines) {
- String[] args = line.trim().split(",");
- if (args.length >= 8) {
- try {
- CpuStats s = new CpuStats();
- s.mTimeStats.put(TimeCategory.USER, Integer.parseInt(args[1]));
- s.mTimeStats.put(TimeCategory.NICE, Integer.parseInt(args[2]));
- s.mTimeStats.put(TimeCategory.SYS, Integer.parseInt(args[3]));
- s.mTimeStats.put(TimeCategory.IDLE, Integer.parseInt(args[4]));
- s.mTimeStats.put(TimeCategory.IOW, Integer.parseInt(args[5]));
- s.mTimeStats.put(TimeCategory.IRQ, Integer.parseInt(args[6]));
- s.mTimeStats.put(TimeCategory.SIRQ, Integer.parseInt(args[7]));
- for (int i = 0; i + 8 < args.length; i += 2) {
- s.mFreqStats.put(Integer.parseInt(args[8 + i]),
- Integer.parseInt(args[9 + i]));
- }
- synchronized(this) {
- if (!mCpuStats.containsKey(args[0])) {
- mCpuStats.put(args[0], new LinkedList<CpuStats>());
- }
- mCpuStats.get(args[0]).add(s);
- }
- } catch (NumberFormatException e) {
- CLog.w("Unexpected input: %s", line.trim());
- } catch (IndexOutOfBoundsException e) {
- CLog.w("Unexpected input: %s", line.trim());
- }
- } else if (args.length > 1 || !"".equals(args[0])) {
- CLog.w("Unexpected input: %s", line.trim());
- }
- }
- }
-
- /**
- * Cancels the {@code cpustats} command.
- */
- public synchronized void cancel() {
- if (mIsCancelled) {
- return;
- }
- mIsCancelled = true;
- if (mLogWriter != null) {
- try {
- mLogWriter.flush();
- mLogWriter.close();
- } catch (IOException e) {
- CLog.e("Error closing writer");
- CLog.e(e);
- } finally {
- mLogWriter = null;
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public synchronized boolean isCancelled() {
- return mIsCancelled;
- }
-
- /**
- * Get all the parsed data as a map from label to lists of {@link CpuStats} objects.
- */
- public synchronized Map<String, List<CpuStats>> getCpuStats() {
- Map<String, List<CpuStats>> copy = new HashMap<String, List<CpuStats>>(
- mCpuStats.size());
- for (String k : mCpuStats.keySet()) {
- copy.put(k, new ArrayList<CpuStats>(mCpuStats.get(k)));
- }
- return copy;
- }
- }
-
- private CpuStatsReceiver mReceiver = new CpuStatsReceiver();
-
- /**
- * Create a {@link CpuStatsCollector}.
- *
- * @param testDevice The test device
- */
- public CpuStatsCollector(ITestDevice testDevice) {
- this(testDevice, 1);
- }
-
- /**
- * Create a {@link CpuStatsCollector} with a delay specified.
- *
- * @param testDevice The test device
- * @param delay The delay time in seconds
- */
- public CpuStatsCollector(ITestDevice testDevice, int delay) {
- super("CpuStatsCollector");
- mTestDevice = testDevice;
- mDelay = delay;
- }
-
- /**
- * Specify a file to log output to.
- *
- * @param logFile the file to log output to.
- */
- public void logToFile(File logFile) {
- mReceiver.logToFile(logFile);
- }
-
- /**
- * Cancels the {@code cpustats} command.
- */
- public synchronized void cancel() {
- mReceiver.cancel();
- }
-
- /**
- * Gets whether the {@code cpustats} command is canceled.
- *
- * @return if the {@code cpustats} command is canceled.
- */
- public synchronized boolean isCancelled() {
- return mReceiver.isCancelled();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void run() {
- try {
- mTestDevice.executeShellCommand(String.format(CPU_STATS_CMD, mDelay), mReceiver);
- } catch (DeviceNotAvailableException e) {
- CLog.e("Device %s not available:", mTestDevice.getSerialNumber());
- CLog.e(e);
- }
- }
-
- /**
- * Get the mapping of labels to lists of {@link CpuStats} instances.
- *
- * @return a mapping of labels to lists of {@link CpuStats} instances. The labels will include
- * "Total" and "cpu0"..."cpuN" for each CPU on the device.
- */
- public Map<String, List<CpuStats>> getCpuStats() {
- return mReceiver.getCpuStats();
- }
-
- /**
- * Get the mean of the total CPU usage for a list of {@link CpuStats}.
- *
- * @param cpuStats the list of {@link CpuStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getTotalPercentageMean(List<CpuStats> cpuStats) {
- SimpleStats stats = new SimpleStats();
- for (CpuStats s : cpuStats) {
- if (s.getTotalUsage() != null) {
- stats.add(s.getTotalUsage());
- }
- }
- return 100 * stats.mean();
- }
-
- /**
- * Get the mean of the user and nice CPU usage for a list of {@link CpuStats}.
- *
- * @param cpuStats the list of {@link CpuStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getUserPercentageMean(List<CpuStats> cpuStats) {
- return (getPercentageMean(cpuStats, TimeCategory.USER) +
- getPercentageMean(cpuStats, TimeCategory.NICE));
- }
-
- /**
- * Get the mean of the system CPU usage for a list of {@link CpuStats}.
- *
- * @param cpuStats the list of {@link CpuStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getSystemPercentageMean(List<CpuStats> cpuStats) {
- return getPercentageMean(cpuStats, TimeCategory.SYS);
- }
-
- /**
- * Get the mean of the iow CPU usage for a list of {@link CpuStats}.
- *
- * @param cpuStats the list of {@link CpuStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getIowPercentageMean(List<CpuStats> cpuStats) {
- return getPercentageMean(cpuStats, TimeCategory.IOW);
- }
-
- /**
- * Get the mean of the IRQ and SIRQ CPU usage for a list of {@link CpuStats}.
- *
- * @param cpuStats the list of {@link CpuStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getIrqPercentageMean(List<CpuStats> cpuStats) {
- return (getPercentageMean(cpuStats, TimeCategory.IRQ) +
- getPercentageMean(cpuStats, TimeCategory.SIRQ));
- }
-
- /**
- * Get the mean of the estimated MHz for a list of {@link CpuStats}.
- *
- * @param cpuStats the list of {@link CpuStats}
- * @return The average estimated MHz in MHz.
- * @see CpuStats#getEstimatedMhz()
- */
- public static Double getEstimatedMhzMean(List<CpuStats> cpuStats) {
- SimpleStats stats = new SimpleStats();
- for (CpuStats s : cpuStats) {
- if (!s.mFreqStats.isEmpty()) {
- stats.add(s.getEstimatedMhz());
- }
- }
- return stats.mean();
- }
-
- /**
- * Get the mean of the used MHz for a list of {@link CpuStats}.
- *
- * @param cpuStats the list of {@link CpuStats}
- * @return The average used MHz as a percentage (0 to 100).
- * @see CpuStats#getUsedMhzPercentage()
- */
- public static Double getUsedMhzPercentageMean(List<CpuStats> cpuStats) {
- SimpleStats stats = new SimpleStats();
- for (CpuStats s : cpuStats) {
- if (!s.mFreqStats.isEmpty()) {
- stats.add(s.getUsedMhzPercentage());
- }
- }
- return stats.mean();
- }
-
- /**
- * Helper method for calculating the percentage mean for a {@link TimeCategory}.
- */
- private static Double getPercentageMean(List<CpuStats> cpuStats, TimeCategory category) {
- SimpleStats stats = new SimpleStats();
- for (CpuStats s : cpuStats) {
- stats.add(s.getPercentage(category));
- }
- return stats.mean();
- }
-
- /**
- * Method to access the receiver used to parse the cpu stats. Used for unit testing.
- */
- CpuStatsReceiver getReceiver() {
- return mReceiver;
- }
-}
diff --git a/src/com/android/tradefed/device/DeviceFatalError.java b/src/com/android/tradefed/device/DeviceFatalError.java
deleted file mode 100644
index 41bb8c4..0000000
--- a/src/com/android/tradefed/device/DeviceFatalError.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-/**
- * Thrown when a fatal error has occurred with device, and it should no longer be used for testing.
- *
- * This should typically be used when the device is still visible through adb, but it is in some
- * known-to-be unrecoverable state. Or if an "interesting" exception condition happened, that
- * requires human inspection while device is in the current state.
- */
-public class DeviceFatalError extends Exception {
-
- private static final long serialVersionUID = -7928528651742852301L;
-
- /**
- * Creates a {@link DeviceFatalError}.
- *
- * @param msg a descriptive error message of the error
- */
- public DeviceFatalError(String msg) {
- super(msg);
- }
-}
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index dc31b9a..41a5096 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -92,7 +92,7 @@
/** a {@link DeviceSelectionOptions} that matches any device. Visible for testing. */
static final IDeviceSelection ANY_DEVICE_OPTIONS = new DeviceSelectionOptions();
- static final String NULL_DEVICE_SERIAL_PREFIX = "null-device";
+ private static final String NULL_DEVICE_SERIAL_PREFIX = "null-device";
private static final String EMULATOR_SERIAL_PREFIX = "emulator";
private static final String TCP_DEVICE_SERIAL_PREFIX = "tcp-device";
private static final String GCE_DEVICE_SERIAL_PREFIX = "gce-device";
diff --git a/src/com/android/tradefed/device/DeviceProperties.java b/src/com/android/tradefed/device/DeviceProperties.java
index 0024272..b4fc512 100644
--- a/src/com/android/tradefed/device/DeviceProperties.java
+++ b/src/com/android/tradefed/device/DeviceProperties.java
@@ -32,4 +32,12 @@
public static final String VARIANT_LEGACY_LESS_EQUAL_O = "ro.product.device";
/** proprty name to indicate SDK version */
public static final String SDK_VERSION = "ro.build.version.sdk";
+ /** property name for device brand */
+ public static final String BRAND = "ro.product.brand";
+ /** property name for device product name */
+ public static final String PRODUCT = "ro.product.name";
+ /** property name for device release version, e.g. version 9 for Android Pie */
+ public static final String RELEASE_VERSION = "ro.build.version.release";
+ /** property name for device boot reason history */
+ public static final String BOOT_REASON_HISTORY = "persist.sys.boot.reason.history";
}
diff --git a/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java b/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java
deleted file mode 100644
index c15394e..0000000
--- a/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java
+++ /dev/null
@@ -1,320 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.command.remote.DeviceDescriptor;
-import com.android.tradefed.config.GlobalConfiguration;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.CircularByteArray;
-
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-
-/**
- * A {@link IDeviceMonitor} that calculates device utilization stats.
- * <p/>
- * Currently measures simple moving average of allocation time % over a 24 hour window.
- */
-public class DeviceUtilStatsMonitor implements IDeviceMonitor {
-
- private static final int INITIAL_DELAY_MS = 5000;
-
- /**
- * Enum for configuring treatment of stub devices when calculating average host utilization
- */
- public enum StubDeviceUtil {
- /** never include stub device data */
- IGNORE,
- /**
- * include stub device data only if any stub device of same type is allocated at least
- * once
- */
- INCLUDE_IF_USED,
- /** always include stub device data */
- ALWAYS_INCLUDE
- }
-
- @Option(name = "collect-null-device", description =
- "controls if null device data should be used when calculating avg host utilization")
- private StubDeviceUtil mCollectNullDevice = StubDeviceUtil.INCLUDE_IF_USED;
-
- @Option(name = "collect-emulator", description =
- "controls if emulator data should be used when calculating avg host utilization")
- private StubDeviceUtil mCollectEmulator = StubDeviceUtil.INCLUDE_IF_USED;
-
- @Option(name = "sample-window-hours", description =
- "the moving average window size to use, in hours")
- private int mSampleWindowHours = 8;
-
- @Option(name = "sample-interval-secs", description =
- "the time period between samples, in seconds")
- private int mSamplingIntervalSec = 60;
-
- private boolean mNullDeviceAllocated = false;
- private boolean mEmulatorAllocated = false;
-
- /**
- * Container for utilization stats.
- */
- public static class UtilizationDesc {
- final int mTotalUtil;
- final Map<String, Integer> mDeviceUtil;
-
- public UtilizationDesc(int totalUtil, Map<String, Integer> deviceUtil) {
- mTotalUtil = totalUtil;
- mDeviceUtil = deviceUtil;
- }
-
- /**
- * Return the total utilization for all devices in TF process, measured as total allocation
- * time for all devices vs total available time.
- *
- * @return percentage utilization
- */
- public int getTotalUtil() {
- return mTotalUtil;
- }
-
- /**
- * Helper method to return percent utilization for a device. Returns 0 if no utilization
- * data exists for device
- */
- public Integer getUtilForDevice(String serial) {
- Integer util = mDeviceUtil.get(serial);
- if (util == null) {
- return 0;
- }
- return util;
- }
- }
-
- private class DeviceUtilRecord {
- // store samples of device util, where 0 = avail, 1 = allocated
- // TODO: save memory by using CircularBitArray
- private CircularByteArray mData;
- private int mConsecutiveMissedSamples = 0;
-
- DeviceUtilRecord() {
- mData = new CircularByteArray(mMaxSamples);
- }
-
- public void addSample(DeviceAllocationState state) {
- if (DeviceAllocationState.Allocated.equals(state)) {
- mData.add((byte)1);
- } else {
- mData.add((byte)0);
- }
- mConsecutiveMissedSamples = 0;
- }
-
- public long getNumAllocations() {
- return mData.getSum();
- }
-
- public long getTotalSamples() {
- return mData.size();
- }
-
- /**
- * Record sample for missing device.
- *
- * @param serial device serial number
- * @return true if sample was added, false if device has been missing for more than max
- * samples
- */
- public boolean addMissingSample(String serial) {
- mConsecutiveMissedSamples++;
- if (mConsecutiveMissedSamples > mMaxSamples) {
- return false;
- }
- mData.add((byte)0);
- return true;
- }
- }
-
- private class SamplingTask extends TimerTask {
- @Override
- public void run() {
- CLog.d("Collecting utilization");
- // track devices that we have records for, but are not reported by device lister
- Map<String, DeviceUtilRecord> goneDevices = new HashMap<>(mDeviceUtilMap);
-
- for (DeviceDescriptor deviceDesc : mDeviceLister.listDevices()) {
- DeviceUtilRecord record = getDeviceRecord(deviceDesc.getSerial());
- record.addSample(deviceDesc.getState());
- goneDevices.remove(deviceDesc.getSerial());
- }
-
- // now record samples for gone devices
- for (Map.Entry<String, DeviceUtilRecord> goneSerialEntry : goneDevices.entrySet()) {
- String serial = goneSerialEntry.getKey();
- if (!goneSerialEntry.getValue().addMissingSample(serial)) {
- CLog.d("Forgetting device %s", serial);
- mDeviceUtilMap.remove(serial);
- }
- }
- }
- }
-
- private int mMaxSamples;
-
- /** a map of device serial to device records */
- private Map<String, DeviceUtilRecord> mDeviceUtilMap = new Hashtable<>();
-
- private DeviceLister mDeviceLister;
-
- private Timer mTimer;
- private SamplingTask mSamplingTask = new SamplingTask();
-
- /**
- * Get the device utilization up to the last 24 hours
- */
- public synchronized UtilizationDesc getUtilizationStats() {
- CLog.d("Calculating device util");
-
- long totalAllocSamples = 0;
- long totalSamples = 0;
- Map<String, Integer> deviceUtilMap = new HashMap<>();
- for (Map.Entry<String, DeviceUtilRecord> deviceRecordEntry : mDeviceUtilMap.entrySet()) {
- if (shouldTrackDevice(deviceRecordEntry.getKey())) {
- long allocSamples = deviceRecordEntry.getValue().getNumAllocations();
- long numSamples = deviceRecordEntry.getValue().getTotalSamples();
- totalAllocSamples += allocSamples;
- totalSamples += numSamples;
- deviceUtilMap.put(deviceRecordEntry.getKey(), getUtil(allocSamples, numSamples));
- }
- }
- return new UtilizationDesc(getUtil(totalAllocSamples, totalSamples), deviceUtilMap);
- }
-
- /**
- * Get device utilization as a percent
- */
- private static int getUtil(long allocSamples, long numSamples) {
- if (numSamples <= 0) {
- return 0;
- }
- return (int)((allocSamples * 100) / numSamples);
- }
-
- @Override
- public void run() {
- calculateMaxSamples();
- mTimer = new Timer();
- mTimer.scheduleAtFixedRate(mSamplingTask, INITIAL_DELAY_MS, mSamplingIntervalSec * 1000);
- }
-
- @Override
- public void stop() {
- if (mTimer != null) {
- mTimer.cancel();
- mTimer.purge();
- }
- }
-
- @Override
- public void setDeviceLister(DeviceLister lister) {
- mDeviceLister = lister;
- }
-
- /**
- * Listens to device state changes and records time that device transitions from or to
- * available or allocated state.
- */
- @Override
- public synchronized void notifyDeviceStateChange(String serial, DeviceAllocationState oldState,
- DeviceAllocationState newState) {
- if (mNullDeviceAllocated && mEmulatorAllocated) {
- // optimization, don't enter calculation below unless needed
- return;
- }
- if (DeviceAllocationState.Allocated.equals(newState)) {
- IDeviceManager dvcMgr = getDeviceManager();
- if (dvcMgr.isNullDevice(serial)) {
- mNullDeviceAllocated = true;
- } else if (dvcMgr.isEmulator(serial)) {
- mEmulatorAllocated = true;
- }
- }
- }
-
- /**
- * Get the device util records for given serial, creating if necessary.
- */
- private DeviceUtilRecord getDeviceRecord(String serial) {
- DeviceUtilRecord r = mDeviceUtilMap.get(serial);
- if (r == null) {
- r = new DeviceUtilRecord();
- mDeviceUtilMap.put(serial, r);
- }
- return r;
- }
-
- private boolean shouldTrackDevice(String serial) {
- IDeviceManager dvcMgr = getDeviceManager();
- if (dvcMgr.isNullDevice(serial)) {
- switch (mCollectNullDevice) {
- case ALWAYS_INCLUDE:
- return true;
- case IGNORE:
- return false;
- case INCLUDE_IF_USED:
- return mNullDeviceAllocated;
- }
- } else if (dvcMgr.isEmulator(serial)) {
- switch (mCollectEmulator) {
- case ALWAYS_INCLUDE:
- return true;
- case IGNORE:
- return false;
- case INCLUDE_IF_USED:
- return mEmulatorAllocated;
- }
- }
- return true;
- }
-
- IDeviceManager getDeviceManager() {
- return GlobalConfiguration.getDeviceManagerInstance();
- }
-
- TimerTask getSamplingTask() {
- return mSamplingTask;
- }
-
- // @VisibleForTesting
- void calculateMaxSamples() {
- // find max samples to collect by converting sample window to seconds then divide by
- // sampling interval
- mMaxSamples = mSampleWindowHours * 60 * 60 / mSamplingIntervalSec;
- assert(mMaxSamples > 0);
- }
-
- // @VisibleForTesting
- void setMaxSamples(int maxSamples) {
- mMaxSamples = maxSamples;
- }
-
- // @VisibleForTesting
- int getMaxSamples() {
- return mMaxSamples;
- }
-}
diff --git a/src/com/android/tradefed/device/ITestDeviceMutator.java b/src/com/android/tradefed/device/ITestDeviceMutator.java
deleted file mode 100644
index fa53850..0000000
--- a/src/com/android/tradefed/device/ITestDeviceMutator.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.ddmlib.IDevice;
-
-/**
- * An interface allowing manipulation of the {@link IManagedTestDevice}.
- * Note that {@link IManagedTestDevice} is not public, {@link ITestDevice} is cast to
- * {@link IManagedTestDevice}.
- * <b>This is a rather dangerous manipulation and not recommended if you have no need for this.</b>
- */
-public interface ITestDeviceMutator {
-
-
- /**
- * Changes the {@link IDevice} held by {@link ITestDevice}
- * @param testDevice
- * @param device
- */
- public void setIDevice(ITestDevice testDevice, IDevice device);
-
- /**
- * Sets if Fastboot is enabled or not for a given {@link ITestDevice}.
- * @param testDevice
- * @param fastbootEnabled
- */
- public void setFastbootEnabled(ITestDevice testDevice, boolean fastbootEnabled);
-}
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index f4c8f37..2ae8e43 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -56,7 +56,6 @@
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.KeyguardControllerState;
import com.android.tradefed.util.ProcessInfo;
-import com.android.tradefed.util.PsParser;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.SizeLimitedOutputStream;
@@ -82,6 +81,7 @@
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
@@ -102,7 +102,7 @@
*/
public class NativeDevice implements IManagedTestDevice {
- private static final String SD_CARD = "/sdcard/";
+ protected static final String SD_CARD = "/sdcard/";
/**
* Allow pauses of up to 2 minutes while receiving bugreport.
* <p/>
@@ -261,17 +261,26 @@
mCmd = cmd;
}
+ private void logExceptionAndOutput(CommandResult result) {
+ CLog.w("Command exited with status: %s", result.getStatus().toString());
+ CLog.w("Command stdout:\n%s\n", result.getStdout());
+ CLog.w("Command stderr:\n%s\n", result.getStderr());
+ }
+
@Override
public boolean run() throws TimeoutException, IOException {
CommandResult result = getRunUtil().runTimedCmd(mTimeout, mCmd);
// TODO: how to determine device not present with command failing for other reasons
if (result.getStatus() == CommandStatus.EXCEPTION) {
- throw new IOException();
+ logExceptionAndOutput(result);
+ throw new IOException("CommandStatus was EXCEPTION, details in host log");
} else if (result.getStatus() == CommandStatus.TIMED_OUT) {
- throw new TimeoutException();
+ logExceptionAndOutput(result);
+ throw new TimeoutException("CommandStatus was TIMED_OUT, details in host log");
} else if (result.getStatus() == CommandStatus.FAILED) {
// interpret as communication failure
- throw new IOException();
+ logExceptionAndOutput(result);
+ throw new IOException("CommandStatus was FAILED, details in host log");
}
mOutput = result.getStdout();
return true;
@@ -3337,7 +3346,7 @@
CLog.w("Property ro.crypto.state is null on device %s", getSerialNumber());
}
- return "encrypted".equals(output);
+ return "encrypted".equals(output.trim());
}
/**
@@ -3354,11 +3363,13 @@
return mIsEncryptionSupported.booleanValue();
}
enableAdbRoot();
- String output = executeShellCommand("vdc cryptfs enablecrypto").trim();
- mIsEncryptionSupported =
- (output != null
- && Pattern.matches("(500)(\\s+)(\\d+)(\\s+)(Usage)(.*)(:)(.*)", output));
+ String output = getProperty("ro.crypto.state");
+ if (output == null || "unsupported".equals(output.trim())) {
+ mIsEncryptionSupported = false;
+ return mIsEncryptionSupported;
+ }
+ mIsEncryptionSupported = true;
return mIsEncryptionSupported;
}
@@ -3850,6 +3861,12 @@
throw new UnsupportedOperationException("No support for user's feature.");
}
+ /** {@inheritDoc} */
+ @Override
+ public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException {
+ throw new UnsupportedOperationException("No support for user's feature.");
+ }
+
/**
* {@inheritDoc}
*/
@@ -4135,11 +4152,9 @@
return device.getClass().getSimpleName();
}
- /**
- * {@inheritDoc}
- */
+ /** {@inheritDoc} */
@Override
- public void preInvocationSetup(IBuildInfo info)
+ public void preInvocationSetup(IBuildInfo info, List<IBuildInfo> testResourceBuildInfos)
throws TargetSetupError, DeviceNotAvailableException {
// Default implementation
mContentProvider = null;
@@ -4153,11 +4168,10 @@
}
}
- /**
- * {@inheritDoc}
- */
+ /** {@inheritDoc} */
@Override
- public void postInvocationTearDown() {
+ public void postInvocationTearDown(Throwable exception) {
+ mIsEncryptionSupported = null;
FileUtil.deleteFile(mExecuteShellCommandLogs);
mExecuteShellCommandLogs = null;
// Default implementation
@@ -4171,6 +4185,11 @@
if (mContentProvider == null) {
return;
}
+ if (exception instanceof DeviceNotAvailableException) {
+ CLog.e(
+ "Skip Tradefed Content Provider teardown due to DeviceNotAvailableException.");
+ return;
+ }
if (TestDeviceState.ONLINE.equals(getDeviceState())) {
mContentProvider.tearDown();
}
@@ -4243,28 +4262,86 @@
return o == null ? "unknown" : o.toString();
}
- /**
- * {@inheritDoc}
- */
- @Override
- public List<ProcessInfo> getProcesses() throws DeviceNotAvailableException {
- return PsParser.getProcesses(executeShellCommand(PS_COMMAND));
- }
-
- /**
- * {@inheritDoc}
- */
+ /** {@inheritDoc} */
@Override
public ProcessInfo getProcessByName(String processName) throws DeviceNotAvailableException {
- List<ProcessInfo> processList = getProcesses();
- for (ProcessInfo processInfo : processList) {
- if (processName.equals(processInfo.getName())) {
- return processInfo;
+ String pidString = getProcessPid(processName);
+ if (pidString == null) {
+ return null;
+ }
+ return new ProcessInfo(
+ getProcessUserByPid(pidString),
+ Integer.parseInt(pidString),
+ processName,
+ getProcessStartTimeByPid(pidString));
+ }
+
+ /** Return the process start time since epoch for the given pid string */
+ private long getProcessStartTimeByPid(String pidString) throws DeviceNotAvailableException {
+ String output = executeShellCommand("stat -c%Z /proc/" + pidString);
+ if (output != null && !output.trim().isEmpty()) {
+ try {
+ return Long.parseLong(output.trim());
+ } catch (NumberFormatException e) {
+ return -1L;
+ }
+ }
+ return -1L;
+ }
+
+ /** Return the process user for the given pid string */
+ private String getProcessUserByPid(String pidString) throws DeviceNotAvailableException {
+ String output = executeShellCommand("stat -c%U /proc/" + pidString);
+ if (output != null && !output.trim().isEmpty()) {
+ try {
+ return output.trim();
+ } catch (NumberFormatException e) {
+ return null;
}
}
return null;
}
+ /** {@inheritDoc} */
+ @Override
+ public Map<Long, String> getBootHistory() throws DeviceNotAvailableException {
+ String output = getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+ /* Sample output:
+ kernel_panic,1556587278
+ reboot,,1556238008
+ reboot,,1556237796
+ reboot,,1556237725
+ */
+ Map<Long, String> bootHistory = new LinkedHashMap<Long, String>();
+ if (Strings.isNullOrEmpty(output)) {
+ return bootHistory;
+ }
+ for (String line : output.split("\\n")) {
+ String infoStr[] = line.split(",");
+ String startStr = infoStr[infoStr.length - 1];
+ try {
+ long startTime = Long.parseLong(startStr.trim());
+ bootHistory.put(startTime, infoStr[0].trim());
+ } catch (NumberFormatException e) {
+ CLog.e("Fail to parse boot time from line %s", line);
+ }
+ }
+ return bootHistory;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Map<Long, String> getBootHistorySince(long utcEpochTime)
+ throws DeviceNotAvailableException {
+ Map<Long, String> bootHistory = new LinkedHashMap<Long, String>();
+ for (Map.Entry<Long, String> entry : getBootHistory().entrySet()) {
+ if (entry.getKey() > utcEpochTime) {
+ bootHistory.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return bootHistory;
+ }
+
/**
* Validates that the given input is a valid MAC address
*
diff --git a/src/com/android/tradefed/device/ReconnectingRecovery.java b/src/com/android/tradefed/device/ReconnectingRecovery.java
deleted file mode 100644
index 56a62c1..0000000
--- a/src/com/android/tradefed/device/ReconnectingRecovery.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.IRunUtil;
-import com.android.tradefed.util.RunUtil;
-
-/**
- * Recovers a device by re-establishing a TCP connection via the adb server on
- * the host.
- */
-public class ReconnectingRecovery implements IDeviceRecovery {
-
- private static final int ADB_TIMEOUT = 2 * 60 * 1000;
- private static final int CONNECTION_ATTEMPTS = 5;
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline)
- throws DeviceNotAvailableException {
- String serial = monitor.getSerialNumber();
-
- // disconnect - many versions of adb client have stale TCP connection
- // status
- getRunUtil().runTimedCmd(ADB_TIMEOUT, "adb", "disconnect", serial);
-
- // try to reconnect
- int attempt = 1;
- do {
- CLog.i("Trying to reconnect with device " + serial + " / attempt " + attempt);
- getRunUtil().runTimedCmd(ADB_TIMEOUT, "adb", "connect", serial);
- } while (monitor.waitForDeviceOnline() == null && ++attempt <= CONNECTION_ATTEMPTS);
-
- String errMsg = "Could not recover device " + serial + " after " + --attempt + " attempts";
-
- // occasionally device is erroneously reported as online - double check
- // that we can shell into device
- if (!monitor.waitForDeviceShell(10 * 1000)) {
- throw new DeviceUnresponsiveException(errMsg, serial);
- }
-
- if (!recoverUntilOnline) {
- if (monitor.waitForDeviceAvailable() == null) {
- throw new DeviceUnresponsiveException(errMsg, serial);
- }
- }
-
- CLog.v("Successfully reconnected with device " + serial);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void recoverDeviceBootloader(IDeviceStateMonitor monitor)
- throws DeviceNotAvailableException {
- throw new java.lang.UnsupportedOperationException(
- "This implementation can't recover a device in bootloader mode.");
- }
-
- /**
- * {@inheritDoc}
- * <p>
- * This implementation assumes devices in recovery mode can't be talked to
- * at all, so it will try to recover a device and leave it in fully booted
- * mode.
- */
- @Override
- public void recoverDeviceRecovery(IDeviceStateMonitor monitor)
- throws DeviceNotAvailableException {
- recoverDevice(monitor, false);
- }
-
- /**
- * Get the {@link RunUtil} instance to use.
- * <p/>
- * Exposed for unit testing.
- */
- IRunUtil getRunUtil() {
- return RunUtil.getDefault();
- }
-}
diff --git a/src/com/android/tradefed/device/RemoteAndroidDevice.java b/src/com/android/tradefed/device/RemoteAndroidDevice.java
index 77b0c05..4b70e28 100644
--- a/src/com/android/tradefed/device/RemoteAndroidDevice.java
+++ b/src/com/android/tradefed/device/RemoteAndroidDevice.java
@@ -63,8 +63,8 @@
}
@Override
- public void postInvocationTearDown() {
- super.postInvocationTearDown();
+ public void postInvocationTearDown(Throwable exception) {
+ super.postInvocationTearDown(exception);
FileUtil.deleteFile(mAdbConnectLogs);
}
diff --git a/src/com/android/tradefed/device/RetryingWaitDeviceRecovery.java b/src/com/android/tradefed/device/RetryingWaitDeviceRecovery.java
deleted file mode 100644
index 313fed5..0000000
--- a/src/com/android/tradefed/device/RetryingWaitDeviceRecovery.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.tradefed.config.Option;
-import com.android.tradefed.log.LogUtil.CLog;
-
-/**
- * A {@link WaitDeviceRecovery} which retries its recovery step either indefinitely
- * or for a certain number of iterations.
- */
-public class RetryingWaitDeviceRecovery extends WaitDeviceRecovery {
-
- @Option(name = "max-wait-iter",
- description = "maximum number of retries for device recovery, 0 for unlimited")
- private int mMaxIters = 0;
-
- @Override
- public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline) {
- int iter = 0;
- while (iter < mMaxIters || mMaxIters == 0) {
- try {
- super.recoverDevice(monitor, recoverUntilOnline);
- return;
- } catch (DeviceNotAvailableException e) {
- CLog.i("Wait attempt %d failed, trying again. Max iterations: %d",
- ++iter, mMaxIters);
- }
- }
- }
-}
-
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index 8df4860..a7dfa4c 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -32,7 +32,6 @@
import com.android.tradefed.util.KeyguardControllerState;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
-import com.android.tradefed.util.UserUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
@@ -1025,6 +1024,17 @@
return packages;
}
+ /** {@inheritDoc} */
+ @Override
+ public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
+ if (deviceFilePath.startsWith(SD_CARD)) {
+ deviceFilePath =
+ deviceFilePath.replaceFirst(
+ SD_CARD, String.format("/storage/emulated/%s/", getCurrentUser()));
+ }
+ return super.doesFileExist(deviceFilePath);
+ }
+
/**
* {@inheritDoc}
*/
@@ -1038,6 +1048,25 @@
return userIds;
}
+ /** {@inheritDoc} */
+ @Override
+ public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException {
+ ArrayList<String[]> lines = tokenizeListUsers();
+ Map<Integer, UserInfo> result = new HashMap<Integer, UserInfo>(lines.size());
+ for (String[] tokens : lines) {
+ UserInfo userInfo =
+ new UserInfo(
+ /* userId= */ Integer.parseInt(tokens[1]),
+ /* userName= */ tokens[2],
+ /* flag= */ Integer.parseInt(tokens[3], 16),
+ /* isRunning= */ tokens.length >= 5
+ ? tokens[4].contains("running")
+ : false);
+ result.put(userInfo.userId(), userInfo);
+ }
+ return result;
+ }
+
/**
* Tokenizes the output of 'pm list users'.
* The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name,
@@ -1299,14 +1328,14 @@
/** {@inheritDoc} */
@Override
public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
- if (userId == UserUtil.USER_SYSTEM) {
+ if (userId == UserInfo.USER_SYSTEM) {
return false;
}
int flags = getUserFlags(userId);
if (flags == INVALID_USER_ID) {
return false;
}
- return (flags & UserUtil.FLAGS_NOT_SECONDARY) == 0;
+ return (flags & UserInfo.FLAGS_NOT_SECONDARY) == 0;
}
/**
@@ -1374,9 +1403,9 @@
// disable keyguard if option is true
prePostBootSetup();
return true;
- } else {
- RunUtil.getDefault().sleep(getCheckNewUserSleep());
}
+ RunUtil.getDefault().sleep(getCheckNewUserSleep());
+ executeShellCommand(String.format("am switch-user %d", userId));
}
CLog.e("User did not switch in the given %d timeout", timeout);
return false;
@@ -1569,8 +1598,8 @@
/** {@inheritDoc} */
@Override
- public void postInvocationTearDown() {
- super.postInvocationTearDown();
+ public void postInvocationTearDown(Throwable exception) {
+ super.postInvocationTearDown(exception);
// If wifi was installed and it's a real device, attempt to clean it.
if (mWasWifiHelperInstalled) {
mWasWifiHelperInstalled = false;
@@ -1580,6 +1609,10 @@
if (!TestDeviceState.ONLINE.equals(getDeviceState())) {
return;
}
+ if (exception instanceof DeviceNotAvailableException) {
+ CLog.e("Skip WifiHelper teardown due to DeviceNotAvailableException.");
+ return;
+ }
try {
// Uninstall the wifi utility if it was installed.
IWifiHelper wifi = createWifiHelper(false);
diff --git a/src/com/android/tradefed/device/TestDeviceMutator.java b/src/com/android/tradefed/device/TestDeviceMutator.java
deleted file mode 100644
index 2044e8d..0000000
--- a/src/com/android/tradefed/device/TestDeviceMutator.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.ddmlib.IDevice;
-
-/**
- * Default implementation of {@link ITestDeviceMutator}
- */
-public class TestDeviceMutator implements ITestDeviceMutator {
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void setIDevice(ITestDevice testDevice, IDevice device) {
- ((IManagedTestDevice)testDevice).setIDevice(device);
- }
-
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void setFastbootEnabled(ITestDevice testDevice, boolean fastbootEnabled) {
- ((IManagedTestDevice)testDevice).setFastbootEnabled(fastbootEnabled);
- }
-
-}
diff --git a/src/com/android/tradefed/device/TopHelper.java b/src/com/android/tradefed/device/TopHelper.java
deleted file mode 100644
index c77b5a4..0000000
--- a/src/com/android/tradefed/device/TopHelper.java
+++ /dev/null
@@ -1,346 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.ddmlib.MultiLineReceiver;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.SimpleStats;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Helper class which runs top continuously on an {@link ITestDevice} and parses the output.
- * <p>
- * Provides a method to record the output of top and get all recorded CPU usage measurements or an
- * average of a specified range of measurements. Note that top can cause approximately a 10%
- * overhead to the CPU usage while running, so results will not be entirely accurate.
- * </p>
- */
-public class TopHelper extends Thread {
- /** The top command to run during the actions. */
- private static final String TOP_CMD = "top -d %d -m 10 -t";
- /** The pattern to match for the top output. */
- private static final Pattern TOP_PERCENT_PATTERN =
- Pattern.compile("User (\\d+)%, System (\\d+)%, IOW (\\d+)%, IRQ (\\d+)%");
-
- private ITestDevice mTestDevice;
- private int mDelay;
-
- /**
- * Enum used for distinguishing between the various percentages in the top output.
- */
- enum PercentCategory {
- TOTAL,
- USER,
- SYSTEM,
- IOW,
- IRQ
- }
-
- /**
- * Class for holding the parsed output for a single top output.
- * <p>
- * Currently, this only holds the percentage info from top but can be extended to contain the
- * process information in the top output.
- * </p>
- */
- public static class TopStats {
- public Double mTotalPercent = null;
- public Double mUserPercent = null;
- public Double mSystemPercent = null;
- public Double mIowPercent = null;
- public Double mIrqPercent = null;
- }
-
- /**
- * Receiver which parses the output from top.
- */
- static class TopReceiver extends MultiLineReceiver {
- private List<TopStats> mTopStats = new LinkedList<TopStats>();
- private boolean mIsCancelled = false;
- private File mLogFile = null;
- private BufferedWriter mLogWriter = null;
-
- public TopReceiver() {
- setTrimLine(false);
- }
-
- /**
- * Specify a file to log the top output to.
- *
- * @param logFile the file to lot output to.
- */
- public synchronized void logToFile(File logFile) {
- try {
- mLogFile = logFile;
- mLogWriter = new BufferedWriter(new FileWriter(mLogFile));
- } catch (IOException e) {
- CLog.e("Error creating fileWriter:");
- CLog.e(e);
- mLogWriter = null;
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void processNewLines(String[] lines) {
- if (mIsCancelled) {
- return;
- }
- synchronized (this) {
- if (mLogWriter != null) {
- try {
- for (String line : lines) {
- mLogWriter.write(line + "\n");
- }
- } catch (IOException e) {
- CLog.e("Error writing to file:");
- CLog.e(e);
- }
- }
- }
- for (String line : lines) {
- line = line.trim();
- Matcher m = TOP_PERCENT_PATTERN.matcher(line);
- if (m.matches()) {
- TopStats s = new TopStats();
-
- // Will not trigger NumberFormatException due to TOP_PATTERN matching.
- s.mUserPercent = Double.parseDouble(m.group(1));
- s.mSystemPercent = Double.parseDouble(m.group(2));
- s.mIowPercent = Double.parseDouble(m.group(3));
- s.mIrqPercent = Double.parseDouble(m.group(4));
- s.mTotalPercent = (s.mUserPercent + s.mSystemPercent + s.mIowPercent +
- s.mIrqPercent);
- synchronized(this) {
- mTopStats.add(s);
- }
- }
- }
- }
-
- /**
- * Cancels the top command.
- */
- public synchronized void cancel() {
- if (mIsCancelled) {
- return;
- }
- mIsCancelled = true;
- if (mLogWriter != null) {
- try {
- mLogWriter.flush();
- mLogWriter.close();
- } catch (IOException e) {
- CLog.e("Error closing writer:");
- CLog.e(e);
- } finally {
- mLogWriter = null;
- }
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public synchronized boolean isCancelled() {
- return mIsCancelled;
- }
-
- /**
- * Gets a list of {@link TopStats} instances.
- *
- * @return a list of {@link TopStats} instances ordered from oldest to newest.
- */
- public synchronized List<TopStats> getTopStats() {
- return new ArrayList<TopStats>(mTopStats);
- }
- }
-
- private TopReceiver mReceiver = new TopReceiver();
-
- /**
- * Create a {@link TopHelper} instance with a delay specified.
- *
- * @param testDevice The device.
- * @param delay The delay time interval for the top command in seconds.
- */
- public TopHelper(ITestDevice testDevice, int delay) {
- super("TopHelper");
- mTestDevice = testDevice;
- mDelay = delay;
- }
-
- /**
- * Create a {@link TopHelper} instance with a default delay of 1 second.
- *
- * @param testDevice The device.
- */
- public TopHelper(ITestDevice testDevice) {
- this(testDevice, 1);
- }
-
- /**
- * Specify a file to log the top output to.
- *
- * @param logFile the file to lot output to.
- */
- public void logToFile(File logFile) {
- mReceiver.logToFile(logFile);
- }
-
- /**
- * Cancels the top command.
- */
- public synchronized void cancel() {
- mReceiver.cancel();
- }
-
- /**
- * Gets whether the top command is canceled.
- *
- * @return if the top command is canceled.
- */
- public synchronized boolean isCancelled() {
- return mReceiver.isCancelled();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void run() {
- try {
- mTestDevice.executeShellCommand(String.format(TOP_CMD, mDelay), mReceiver);
- } catch (DeviceNotAvailableException e) {
- CLog.e("Device %s not available:", mTestDevice.getSerialNumber());
- CLog.e(e);
- }
- }
-
- /**
- * Gets a list of {@link TopStats} instances.
- *
- * @return a list of {@link TopStats} instances ordered from oldest to newest.
- */
- public List<TopStats> getTopStats() {
- return mReceiver.getTopStats();
- }
-
- /**
- * Get the average total CPU usage for a list of {@link TopStats}.
- *
- * @param topStats the list of {@link TopStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getTotalAverage(List<TopStats> topStats) {
- return getAveragePercentage(topStats, PercentCategory.TOTAL);
- }
-
- /**
- * Get the average user CPU usage for a list of {@link TopStats}.
- *
- * @param topStats the list of {@link TopStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getUserAverage(List<TopStats> topStats) {
- return getAveragePercentage(topStats, PercentCategory.USER);
- }
-
- /**
- * Get the average system CPU usage for a list of {@link TopStats}.
- *
- * @param topStats the list of {@link TopStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getSystemAverage(List<TopStats> topStats) {
- return getAveragePercentage(topStats, PercentCategory.SYSTEM);
- }
-
- /**
- * Get the average IOW CPU usage for a list of {@link TopStats}.
- *
- * @param topStats the list of {@link TopStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getIowAverage(List<TopStats> topStats) {
- return getAveragePercentage(topStats, PercentCategory.IOW);
- }
-
- /**
- * Get the average IRQ CPU usage for a list of {@link TopStats}.
- *
- * @param topStats the list of {@link TopStats}
- * @return The average usage as a percentage (0 to 100).
- */
- public static Double getIrqAverage(List<TopStats> topStats) {
- return getAveragePercentage(topStats, PercentCategory.IRQ);
- }
-
-
- /**
- * Get the average CPU usage for a list of {@link TopStats} and a given category.
- *
- * @param topStats the list of {@link TopStats}
- * @param category the percentage category
- * @return The average usage as a percentage (0 to 100).
- */
- private static Double getAveragePercentage(List<TopStats> topStats, PercentCategory category)
- throws IndexOutOfBoundsException {
- SimpleStats stats = new SimpleStats();
- for (TopStats s : topStats) {
- switch(category) {
- case TOTAL:
- stats.add(s.mTotalPercent);
- break;
- case USER:
- stats.add(s.mUserPercent);
- break;
- case SYSTEM:
- stats.add(s.mSystemPercent);
- break;
- case IOW:
- stats.add(s.mIowPercent);
- break;
- case IRQ:
- stats.add(s.mIrqPercent);
- break;
- }
- }
- return stats.mean();
- }
-
- /**
- * Package protected method used for testing.
- *
- * @return the TopReceiver
- */
- TopReceiver getReceiver() {
- return mReceiver;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/tradefed/device/WifiHelper.java b/src/com/android/tradefed/device/WifiHelper.java
index d2122b7..e98e28d 100644
--- a/src/com/android/tradefed/device/WifiHelper.java
+++ b/src/com/android/tradefed/device/WifiHelper.java
@@ -461,7 +461,6 @@
}
if (!asBool(runWifiUtil("connectToNetwork", "ssid", ssid, "psk", psk, "urlToCheck",
urlToCheck, "scan_ssid", Boolean.toString(scanSsid)))) {
- CLog.e("Failed to connect to " + ssid);
return false;
}
return true;
@@ -473,7 +472,6 @@
@Override
public boolean disconnectFromNetwork() throws DeviceNotAvailableException {
if (!asBool(runWifiUtil("disconnectFromNetwork"))) {
- CLog.e("Failed to disconnect");
return false;
}
if (!disableWifi()) {
@@ -524,7 +522,11 @@
WifiUtilOutput parser = new WifiUtilOutput();
mDevice.executeShellCommand(cmd, parser, WIFIUTIL_CMD_TIMEOUT_MINUTES, TimeUnit.MINUTES, 0);
if (parser.getError() != null) {
- CLog.e(parser.getError());
+ String errorMessage =
+ String.format(
+ "Failed to %s due to: '%s'. See logcat for details.",
+ method, parser.getError());
+ CLog.e(errorMessage);
}
return parser.getResult();
}
diff --git a/src/com/android/tradefed/device/cloud/GceManager.java b/src/com/android/tradefed/device/cloud/GceManager.java
index ccf9403..508170f 100644
--- a/src/com/android/tradefed/device/cloud/GceManager.java
+++ b/src/com/android/tradefed/device/cloud/GceManager.java
@@ -222,6 +222,14 @@
getTestDeviceOptions().getInstanceType())) {
gceArgs.add("--avd-type");
gceArgs.add("cheeps");
+
+ if (getTestDeviceOptions().getCrosUser() != null
+ && getTestDeviceOptions().getCrosPassword() != null) {
+ gceArgs.add("--user");
+ gceArgs.add(getTestDeviceOptions().getCrosUser());
+ gceArgs.add("--password");
+ gceArgs.add(getTestDeviceOptions().getCrosPassword());
+ }
}
// If args passed by gce-driver-param do not contain build_id or branch,
diff --git a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
index 9e00ac1..8e2b8f0 100644
--- a/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/ManagedRemoteDevice.java
@@ -17,11 +17,16 @@
import com.android.ddmlib.IDevice;
import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Configuration;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IDeviceMonitor;
import com.android.tradefed.device.IDeviceStateMonitor;
import com.android.tradefed.device.StubDevice;
import com.android.tradefed.device.TestDevice;
+import com.android.tradefed.device.TestDeviceOptions;
import com.android.tradefed.device.cloud.GceAvdInfo.GceStatus;
import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
@@ -49,6 +54,9 @@
private GceAvdInfo mGceAvd;
private ITestLogger mTestLogger;
+ private TestDeviceOptions mCopiedOptions;
+ private IConfiguration mValidationConfig;
+
/**
* Creates a {@link ManagedRemoteDevice}.
*
@@ -66,28 +74,27 @@
throws TargetSetupError, DeviceNotAvailableException {
super.preInvocationSetup(info, testResourceBuildInfos);
mGceAvd = null;
-
+ // First get the options
+ TestDeviceOptions options = getOptions();
// We create a brand new GceManager each time to ensure clean state.
- mGceHandler =
- new GceManager(getDeviceDescriptor(), getOptions(), info, testResourceBuildInfos);
+ mGceHandler = new GceManager(getDeviceDescriptor(), options, info, testResourceBuildInfos);
getGceHandler().logStableHostImageInfos(info);
setFastbootEnabled(false);
// Launch GCE helper script.
long startTime = getCurrentTime();
launchGce();
- long remainingTime = getOptions().getGceCmdTimeout() - (getCurrentTime() - startTime);
+ long remainingTime = options.getGceCmdTimeout() - (getCurrentTime() - startTime);
if (remainingTime < 0) {
throw new DeviceNotAvailableException(
- String.format(
- "Failed to launch GCE after %sms", getOptions().getGceCmdTimeout()),
+ String.format("Failed to launch GCE after %sms", options.getGceCmdTimeout()),
getSerialNumber());
}
}
/** {@inheritDoc} */
@Override
- public void postInvocationTearDown() {
+ public void postInvocationTearDown(Throwable exception) {
try {
CLog.i("Shutting down GCE device %s", getSerialNumber());
// Log the last part of the logcat from the tear down.
@@ -124,8 +131,11 @@
getGceHandler().cleanUp();
}
} finally {
+ if (mValidationConfig != null) {
+ mValidationConfig.cleanDynamicOptionFiles();
+ }
// Ensure parent postInvocationTearDown is always called.
- super.postInvocationTearDown();
+ super.postInvocationTearDown(exception);
}
}
@@ -192,4 +202,25 @@
GceManager getGceHandler() {
return mGceHandler;
}
+
+ /**
+ * Override the base getter to be able to resolve dynamic options before attempting to do the
+ * remote setup.
+ */
+ @Override
+ public TestDeviceOptions getOptions() {
+ if (mCopiedOptions == null) {
+ mCopiedOptions = new TestDeviceOptions();
+ TestDeviceOptions options = super.getOptions();
+ OptionCopier.copyOptionsNoThrow(options, mCopiedOptions);
+ mValidationConfig = new Configuration("validation", "validation");
+ mValidationConfig.setDeviceOptions(mCopiedOptions);
+ try {
+ mValidationConfig.resolveDynamicOptions();
+ } catch (ConfigurationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return mCopiedOptions;
+ }
}
diff --git a/src/com/android/tradefed/device/cloud/MultiUserSetupUtil.java b/src/com/android/tradefed/device/cloud/MultiUserSetupUtil.java
index 3a69667..abbcbfe 100644
--- a/src/com/android/tradefed/device/cloud/MultiUserSetupUtil.java
+++ b/src/com/android/tradefed/device/cloud/MultiUserSetupUtil.java
@@ -118,7 +118,7 @@
TestDeviceOptions options,
IRunUtil runUtil,
long timeoutMs) {
- StringBuilder copyCommandBuilder = new StringBuilder("sudo cp ");
+ StringBuilder copyCommandBuilder = new StringBuilder("sudo cp --reflink=auto ");
for (String file : FILE_TO_BE_COPIED) {
copyCommandBuilder.append(" /home/" + mainRootUser + "/" + file);
}
diff --git a/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java b/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
index ebf577e..70d319e 100644
--- a/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
+++ b/src/com/android/tradefed/device/cloud/NestedRemoteDevice.java
@@ -164,7 +164,7 @@
// Reset recovery since it's a new device
setRecoveryMode(RecoveryMode.AVAILABLE);
try {
- preInvocationSetup(info);
+ preInvocationSetup(info, null);
} catch (TargetSetupError e) {
CLog.e("Failed to re-init the device %s", getSerialNumber());
CLog.e(e);
diff --git a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
index 4ba6cfc..d68a353 100644
--- a/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
+++ b/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDevice.java
@@ -82,15 +82,9 @@
/** {@inheritDoc} */
@Override
- public void preInvocationSetup(IBuildInfo info)
- throws TargetSetupError, DeviceNotAvailableException {
- preInvocationSetup(info, null);
- }
-
- /** {@inheritDoc} */
- @Override
public void preInvocationSetup(IBuildInfo info, List<IBuildInfo> testResourceBuildInfos)
throws TargetSetupError, DeviceNotAvailableException {
+ super.preInvocationSetup(info, testResourceBuildInfos);
try {
mGceAvd = null;
mGceSshMonitor = null;
@@ -155,7 +149,7 @@
/** {@inheritDoc} */
@Override
- public void postInvocationTearDown() {
+ public void postInvocationTearDown(Throwable exception) {
try {
CLog.i("Invocation tear down for device %s", getSerialNumber());
// Log the last part of the logcat from the tear down.
@@ -214,7 +208,7 @@
}
} finally {
// Ensure parent postInvocationTearDown is always called.
- super.postInvocationTearDown();
+ super.postInvocationTearDown(exception);
}
}
@@ -358,19 +352,20 @@
String.format(
CommonLogRemoteFileUtil.NESTED_REMOTE_LOG_DIR,
getOptions().getInstanceUser())
- + "tombstones";
- File tombstonesDir =
- RemoteFileUtil.fetchRemoteDir(
- mGceAvd,
- getOptions(),
- getRunUtil(),
- FETCH_TOMBSTONES_TIMEOUT_MS,
- remoteRuntimePath);
- if (tombstonesDir == null) {
- CLog.e("Pulled tombstone dir was not valid. Path: %s", tombstonesDir);
+ + "tombstones/*";
+ File localDir = null;
+ try {
+ localDir = FileUtil.createTempDir("tombstones");
+ } catch (IOException e) {
+ CLog.e(e);
+ return tombs;
+ }
+ if (!fetchRemoteDir(localDir, remoteRuntimePath)) {
+ CLog.e("Failed to pull %s", remoteRuntimePath);
+ FileUtil.recursiveDelete(localDir);
} else {
- // TODO: If possible delete the tombstones parent temp dir.
- tombs.addAll(Arrays.asList(tombstonesDir.listFiles()));
+ tombs.addAll(Arrays.asList(localDir.listFiles()));
+ localDir.deleteOnExit();
}
return tombs;
}
@@ -378,6 +373,17 @@
return super.getTombstones();
}
+ @VisibleForTesting
+ boolean fetchRemoteDir(File localDir, String remotePath) {
+ return RemoteFileUtil.fetchRemoteDir(
+ mGceAvd,
+ getOptions(),
+ getRunUtil(),
+ FETCH_TOMBSTONES_TIMEOUT_MS,
+ remotePath,
+ localDir);
+ }
+
/**
* Returns the {@link com.android.tradefed.device.cloud.GceSshTunnelMonitor} of the device.
* Exposed for testing.
diff --git a/src/com/android/tradefed/device/cloud/RemoteFileUtil.java b/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
index 5330992..0443c43 100644
--- a/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
+++ b/src/com/android/tradefed/device/cloud/RemoteFileUtil.java
@@ -104,6 +104,36 @@
* @param runUtil a {@link IRunUtil} to execute commands.
* @param timeout in millisecond for the fetch to complete
* @param remoteDirPath The remote path where to find the directory.
+ * @param localDir The local directory where to put the pulled files.
+ * @return True if successful, False otherwise
+ */
+ public static boolean fetchRemoteDir(
+ GceAvdInfo remoteInstance,
+ TestDeviceOptions options,
+ IRunUtil runUtil,
+ long timeout,
+ String remoteDirPath,
+ File localDir) {
+ return internalScpExec(
+ remoteInstance,
+ options,
+ Arrays.asList("-r"),
+ runUtil,
+ timeout,
+ remoteDirPath,
+ localDir,
+ ScpMode.PULL);
+ }
+
+ /**
+ * Fetch a remote directory from the remote host.
+ *
+ * @param remoteInstance The {@link GceAvdInfo} that describe the device.
+ * @param options a {@link TestDeviceOptions} describing the device options to be used for the
+ * GCE device.
+ * @param runUtil a {@link IRunUtil} to execute commands.
+ * @param timeout in millisecond for the fetch to complete
+ * @param remoteDirPath The remote path where to find the directory.
* @return The pulled directory {@link File} if successful, null otherwise
*/
public static File fetchRemoteDir(
diff --git a/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java b/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
index 1e4d16c..3a64b07 100644
--- a/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/BaseDeviceMetricCollector.java
@@ -18,6 +18,7 @@
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.StubDevice;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -33,6 +34,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Base implementation of {@link IMetricCollector} that allows to start and stop collection on
@@ -65,6 +67,7 @@
private List<String> mTestCaseExcludeAnnotationGroup = new ArrayList<>();
private IInvocationContext mContext;
+ private List<ITestDevice> mRealDeviceList;
private ITestInvocationListener mForwarder;
private DeviceMetricData mRunData;
private DeviceMetricData mTestData;
@@ -74,12 +77,19 @@
* Variable for whether or not to skip the collection of one test case because it was filtered.
*/
private boolean mSkipTestCase = false;
+ /** Whether or not the collector was initialized already or not. */
+ private boolean mWasInitDone = false;
@Override
public ITestInvocationListener init(
IInvocationContext context, ITestInvocationListener listener) {
mContext = context;
mForwarder = listener;
+ if (mWasInitDone) {
+ throw new IllegalStateException(
+ String.format("init was called a second time on %s", this));
+ }
+ mWasInitDone = true;
return this;
}
@@ -88,6 +98,18 @@
return mContext.getDevices();
}
+ /** Returns all the non-stub devices from the {@link #getDevices()} list. */
+ public final List<ITestDevice> getRealDevices() {
+ if (mRealDeviceList == null) {
+ mRealDeviceList =
+ mContext.getDevices()
+ .stream()
+ .filter(d -> (!(d.getIDevice() instanceof StubDevice)))
+ .collect(Collectors.toList());
+ }
+ return mRealDeviceList;
+ }
+
@Override
public final List<IBuildInfo> getBuildInfos() {
return mContext.getBuildInfos();
@@ -130,6 +152,15 @@
// Does nothing
}
+ @Override
+ public void onTestEnd(
+ DeviceMetricData testData,
+ final Map<String, Metric> currentTestCaseMetrics,
+ TestDescription test) {
+ // Call the default implementation of onTestEnd if not overridden
+ onTestEnd(testData, currentTestCaseMetrics);
+ }
+
/** =================================== */
/** Invocation Listeners for forwarding */
@Override
@@ -233,7 +264,7 @@
TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
if (!mSkipTestCase) {
try {
- onTestEnd(mTestData, testMetrics);
+ onTestEnd(mTestData, testMetrics, test);
mTestData.addToMetrics(testMetrics);
} catch (Throwable t) {
// Prevent exception from messing up the status reporting.
diff --git a/src/com/android/tradefed/device/metric/IMetricCollector.java b/src/com/android/tradefed/device/metric/IMetricCollector.java
index 387e536..1c8b84f 100644
--- a/src/com/android/tradefed/device/metric/IMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/IMetricCollector.java
@@ -113,4 +113,18 @@
*/
public void onTestEnd(
DeviceMetricData testData, final Map<String, Metric> currentTestCaseMetrics);
+
+ /**
+ * Callback when a test case is ended. This should be the time for clean up.
+ *
+ * @param testData the {@link DeviceMetricData} holding the data for the test case. Will be the
+ * same object as during {@link #onTestStart(DeviceMetricData)}.
+ * @param currentTestCaseMetrics the current map of metrics passed to {@link
+ * #testEnded(TestDescription, Map)}.
+ * @param test the {@link TestDescription} of the test case in progress.
+ */
+ public void onTestEnd(
+ DeviceMetricData testData,
+ final Map<String, Metric> currentTestCaseMetrics,
+ TestDescription test);
}
diff --git a/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java b/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
index 3dcc717..066b425 100644
--- a/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
+++ b/src/com/android/tradefed/device/metric/LogcatOnFailureCollector.java
@@ -43,7 +43,7 @@
@Override
public void onTestRunStart(DeviceMetricData runData) {
- for (ITestDevice device : getDevices()) {
+ for (ITestDevice device : getRealDevices()) {
// In case of multiple runs for the same test runner, re-init the receiver.
initReceiver(device);
// Get the current offset of the buffer to be able to query later
@@ -62,7 +62,7 @@
@Override
public void onTestFail(DeviceMetricData testData, TestDescription test) {
- for (ITestDevice device : getDevices()) {
+ for (ITestDevice device : getRealDevices()) {
// Delay slightly for the error to get in the logcat
getRunUtil().sleep(100);
try (InputStreamSource logcatSource =
diff --git a/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java b/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
index 5b3af5f..d382cfb 100644
--- a/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
@@ -27,6 +27,7 @@
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.Pair;
import com.android.tradefed.util.RunUtil;
@@ -47,11 +48,15 @@
private static final String LINE_SEPARATOR = "\\r?\\n";
private static final char KEY_VALUE_SEPARATOR = ':';
+ private static final String EXTRACTOR_STATUS = "trace_extractor_status";
+ private static final String EXTRACTOR_SUCCESS = "1";
+ private static final String EXTRACTOR_FAILURE = "0";
+ private static final String EXTRACTOR_RUNTIME = "trace_extractor_runtime";
@Option(
name = "perfetto-binary-path",
description = "Path to the script files used to analyze the trace files.")
- private List<String> mScriptPaths = new ArrayList<>();
+ private List<File> mScriptFiles = new ArrayList<>();
@Option(
name = "perfetto-metric-prefix",
@@ -87,9 +92,12 @@
public void processMetricFile(String key, File metricFile,
DeviceMetricData data) {
// Extract the metrics from the trace file.
- for (String scriptPath : mScriptPaths) {
+ for (File scriptFile : mScriptFiles) {
+ // Apply necessary execute permissions to the script.
+ FileUtil.chmodGroupRWX(scriptFile);
+
List<String> commandArgsList = new ArrayList<String>();
- commandArgsList.add(scriptPath);
+ commandArgsList.add(scriptFile.getAbsolutePath());
commandArgsList.add("-trace_file");
commandArgsList.add(metricFile.getAbsolutePath());
@@ -98,12 +106,26 @@
commandArgsList.add(Joiner.on(",").join(mProcessNames));
}
+ String traceExtractorStatus = EXTRACTOR_SUCCESS;
+
+ double scriptDuration = 0;
+ double scriptStartTime = System.currentTimeMillis();
CommandResult cr = runHostCommand(commandArgsList.toArray(new String[commandArgsList
.size()]));
+ scriptDuration = System.currentTimeMillis() - scriptStartTime;
+
+ // Update the script duration metrics.
+ Metric.Builder metricDurationBuilder = Metric.newBuilder();
+ metricDurationBuilder.getMeasurementsBuilder().setSingleDouble(scriptDuration);
+ data.addMetric(
+ String.format("%s_%s", mMetricPrefix, EXTRACTOR_RUNTIME),
+ metricDurationBuilder.setType(DataType.RAW));
+
if (CommandStatus.SUCCESS.equals(cr.getStatus())) {
String[] metrics = cr.getStdout().split(LINE_SEPARATOR);
for (String metric : metrics) {
Pair<String, String> kv = splitKeyValue(metric);
+
if (kv != null) {
Metric.Builder metricBuilder = Metric.newBuilder();
metricBuilder.getMeasurementsBuilder().setSingleString(kv.second);
@@ -116,9 +138,16 @@
}
CLog.i(cr.getStdout());
} else {
+ traceExtractorStatus = EXTRACTOR_FAILURE;
CLog.e("Unable to parse the trace file %s due to %s - Status - %s ",
metricFile.getName(), cr.getStderr(), cr.getStatus());
}
+
+ Metric.Builder metricStatusBuilder = Metric.newBuilder();
+ metricStatusBuilder.getMeasurementsBuilder().setSingleString(traceExtractorStatus);
+ data.addMetric(
+ String.format("%s_%s", mMetricPrefix, EXTRACTOR_STATUS),
+ metricStatusBuilder.setType(DataType.RAW));
}
// Upload and delete the host trace file.
diff --git a/src/com/android/tradefed/device/metric/RebootReasonCollector.java b/src/com/android/tradefed/device/metric/RebootReasonCollector.java
index faf849c..ae2c2f7 100644
--- a/src/com/android/tradefed/device/metric/RebootReasonCollector.java
+++ b/src/com/android/tradefed/device/metric/RebootReasonCollector.java
@@ -43,6 +43,7 @@
public class RebootReasonCollector extends BaseDeviceMetricCollector {
private static final String METRIC_SEP = "-";
public static final String METRIC_PREFIX = "rebooted" + METRIC_SEP;
+ public static final String COUNT_KEY = String.join(METRIC_SEP, "reboot", "count");
private List<ITestDevice> mTestDevices;
// Map to store statsd config ids for each device, keyed by the device serial number.
@@ -84,9 +85,12 @@
"Failed to pull metric data from device %s. Exception: %s.",
device.getSerialNumber(), e.toString());
}
+ Map<String, Integer> metricsForDevice = new HashMap<>();
+ int rebootCount = 0;
for (EventMetricData eventMetricEntry : metricData) {
Atom eventAtom = eventMetricEntry.getAtom();
if (eventAtom.hasBootSequenceReported()) {
+ rebootCount += 1;
BootSequenceReported bootAtom = eventAtom.getBootSequenceReported();
String bootReasonKey =
METRIC_PREFIX
@@ -94,24 +98,20 @@
METRIC_SEP,
bootAtom.getBootloaderReason(),
bootAtom.getSystemReason());
- // Append the device serial number only if there are more than one device.
- if (mTestDevices.size() > 1) {
- bootReasonKey =
- String.join(METRIC_SEP, bootReasonKey, device.getSerialNumber());
- }
- currentRunMetrics.computeIfPresent(
- bootReasonKey,
- (key, metric) ->
- stringToMetric(
- String.valueOf(
- Integer.valueOf(
- metric.getMeasurements()
- .getSingleString())
- + 1)));
- currentRunMetrics.computeIfAbsent(
- bootReasonKey, key -> stringToMetric(String.valueOf(1)));
+ // Update the counts for the specific boot reason in the current atom.
+ metricsForDevice.computeIfPresent(bootReasonKey, (k, v) -> v + 1);
+ metricsForDevice.computeIfAbsent(bootReasonKey, k -> 1);
}
}
+ for (String key : metricsForDevice.keySet()) {
+ runData.addMetricForDevice(
+ device,
+ key,
+ stringToMetric(String.valueOf(metricsForDevice.get(key))).toBuilder());
+ }
+ // Add the count regardless of whether reboots occurred or not.
+ runData.addMetricForDevice(
+ device, COUNT_KEY, stringToMetric(String.valueOf(rebootCount)).toBuilder());
try {
removeConfig(device, configId);
} catch (DeviceNotAvailableException e) {
diff --git a/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollector.java b/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollector.java
index 877a153..761034b 100644
--- a/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollector.java
+++ b/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollector.java
@@ -29,7 +29,7 @@
@Override
public void onTestFail(DeviceMetricData testData, TestDescription test) {
- for (ITestDevice device : getDevices()) {
+ for (ITestDevice device : getRealDevices()) {
try (InputStreamSource screenSource = device.getScreenshot()) {
super.testLog(
String.format(NAME_FORMAT, test.toString(), device.getSerialNumber()),
diff --git a/src/com/android/tradefed/host/OWNERS b/src/com/android/tradefed/host/OWNERS
deleted file mode 100644
index a07eedf..0000000
--- a/src/com/android/tradefed/host/OWNERS
+++ /dev/null
@@ -1,4 +0,0 @@
-# host/ drives host related setup or configuration
-fangk@google.com
-jeffreylu@google.com
-xingdai@google.com
diff --git a/src/com/android/tradefed/invoker/IInvocationExecution.java b/src/com/android/tradefed/invoker/IInvocationExecution.java
index 2107a21..da9d0e4 100644
--- a/src/com/android/tradefed/invoker/IInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/IInvocationExecution.java
@@ -94,14 +94,15 @@
throws DeviceNotAvailableException, TargetSetupError {}
/**
- * Invoke the {@link ITestDevice#postInvocationTearDown()} for each device part of the
+ * Invoke the {@link ITestDevice#postInvocationTearDown(Throwable)} for each device part of the
* invocation.
*
* @param context the {@link IInvocationContext} of the invocation.
* @param config the {@link IConfiguration} of this test run.
+ * @param exception the original exception thrown by the test running if any.
*/
public default void runDevicePostInvocationTearDown(
- IInvocationContext context, IConfiguration config) {}
+ IInvocationContext context, IConfiguration config, Throwable exception) {}
/**
* Execute the target_preparer and multi_target_preparer teardown step. Does the devices tear
@@ -141,11 +142,15 @@
*
* @param config the current {@link IConfiguration}.
* @param context the {@link IInvocationContext} holding the info of the tests.
- * @param rescheduler the {@link IRescheduler}
+ * @param rescheduler the {@link IRescheduler}.
+ * @param logger {@link ITestLogger} used to log file during sharding.
* @return true if test was sharded. Otherwise return <code>false</code>
*/
public default boolean shardConfig(
- IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
+ IConfiguration config,
+ IInvocationContext context,
+ IRescheduler rescheduler,
+ ITestLogger logger) {
return false;
}
diff --git a/src/com/android/tradefed/invoker/InvocationContext.java b/src/com/android/tradefed/invoker/InvocationContext.java
index 3293fbe..18816a3 100644
--- a/src/com/android/tradefed/invoker/InvocationContext.java
+++ b/src/com/android/tradefed/invoker/InvocationContext.java
@@ -23,6 +23,7 @@
import com.android.tradefed.config.proto.ConfigurationDescription.Metadata;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
import com.android.tradefed.invoker.proto.InvocationContext.Context;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.suite.ITestSuite;
@@ -343,6 +344,14 @@
mLocked = false;
}
+ /** Log the {@link InvocationMetricLogger} attributes to the invocation. */
+ public void logInvocationMetrics() {
+ Map<String, String> metrics = InvocationMetricLogger.getInvocationMetrics();
+ if (!metrics.isEmpty()) {
+ mInvocationAttributes.putAll(new MultiMap<>(metrics));
+ }
+ }
+
/** {@inheritDoc} */
@Override
public void addSerialsFromShard(Integer index, List<String> serials) {
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index abf19dc..f52555b 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -161,8 +161,11 @@
@Override
public boolean shardConfig(
- IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
- return createShardHelper().shardConfig(config, context, rescheduler);
+ IConfiguration config,
+ IInvocationContext context,
+ IRescheduler rescheduler,
+ ITestLogger logger) {
+ return createShardHelper().shardConfig(config, context, rescheduler, logger);
}
/** Create an return the {@link IShardHelper} to be used. */
@@ -244,27 +247,23 @@
if (device instanceof ITestLoggerReceiver) {
((ITestLoggerReceiver) context.getDevice(deviceName)).setTestLogger(logger);
}
- if (!config.getCommandOptions().shouldSkipPreDeviceSetup()) {
- device.preInvocationSetup(
- context.getBuildInfo(deviceName),
- context.getBuildInfos()
- .stream()
- .filter(buildInfo -> buildInfo.isTestResourceBuild())
- .collect(Collectors.toList()));
- }
+ device.preInvocationSetup(
+ context.getBuildInfo(deviceName),
+ context.getBuildInfos()
+ .stream()
+ .filter(buildInfo -> buildInfo.isTestResourceBuild())
+ .collect(Collectors.toList()));
}
}
/** {@inheritDoc} */
@Override
public final void runDevicePostInvocationTearDown(
- IInvocationContext context, IConfiguration config) {
+ IInvocationContext context, IConfiguration config, Throwable exception) {
// Extra tear down step for the device
for (String deviceName : context.getDeviceConfigNames()) {
ITestDevice device = context.getDevice(deviceName);
- if (!config.getCommandOptions().shouldSkipPreDeviceSetup()) {
- device.postInvocationTearDown();
- }
+ device.postInvocationTearDown(exception);
}
}
@@ -390,7 +389,7 @@
}
// Extra tear down step for the device
- runDevicePostInvocationTearDown(context, config);
+ runDevicePostInvocationTearDown(context, config, exception);
// After all, run the multi_pre_target_preparer tearDown.
List<IMultiTargetPreparer> multiPrePreparers = config.getMultiPreTargetPreparers();
diff --git a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
index f615387..f82442c 100644
--- a/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
+++ b/src/com/android/tradefed/invoker/RemoteInvocationExecution.java
@@ -19,6 +19,7 @@
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.StubBuildProvider;
+import com.android.tradefed.clearcut.ClearcutClient;
import com.android.tradefed.command.CommandOptions;
import com.android.tradefed.command.CommandRunner;
import com.android.tradefed.config.GlobalConfiguration;
@@ -295,7 +296,7 @@
Throwable exception)
throws Throwable {
// Only run device post invocation teardown
- super.runDevicePostInvocationTearDown(context, config);
+ super.runDevicePostInvocationTearDown(context, config, exception);
}
@Override
@@ -329,6 +330,8 @@
new StringBuilder("TF_GLOBAL_CONFIG=" + globalConfig.getName());
// Set an env variable to notify that this a remote environment.
tfCmdBuilder.append(" " + REMOTE_VM_VARIABLE + "=1");
+ // Disable clearcut in the remote
+ tfCmdBuilder.append(" " + ClearcutClient.DISABLE_CLEARCUT_KEY + "=1");
tfCmdBuilder.append(" ENTRY_CLASS=" + CommandRunner.class.getCanonicalName());
tfCmdBuilder.append(" ./tradefed.sh " + mRemoteTradefedDir + configFile.getName());
if (config.getCommandOptions().shouldUseRemoteSandboxMode()) {
@@ -351,45 +354,57 @@
// Monitor the remote invocation to ensure it's completing. Block until timeout or stops
// running.
- boolean stillRunning =
- isStillRunning(
- currentInvocationListener, configFile, info, options, runUtil, config);
+ boolean stillRunning = true;
+ try {
+ stillRunning =
+ isStillRunning(
+ currentInvocationListener,
+ configFile,
+ info,
+ options,
+ runUtil,
+ config,
+ context);
+ } finally {
+ // Fetch the logs for debugging
+ File stdoutFile =
+ RemoteFileUtil.fetchRemoteFile(
+ info,
+ options,
+ runUtil,
+ PULL_RESULT_TIMEOUT,
+ mRemoteTradefedDir + STDOUT_FILE);
+ if (stdoutFile != null) {
+ try (InputStreamSource source = new FileInputStreamSource(stdoutFile, true)) {
+ currentInvocationListener.testLog(STDOUT_FILE, LogDataType.TEXT, source);
+ }
+ }
- // Fetch the logs
- File stdoutFile =
- RemoteFileUtil.fetchRemoteFile(
- info,
- options,
- runUtil,
- PULL_RESULT_TIMEOUT,
- mRemoteTradefedDir + STDOUT_FILE);
- if (stdoutFile != null) {
- try (InputStreamSource source = new FileInputStreamSource(stdoutFile, true)) {
- currentInvocationListener.testLog(STDOUT_FILE, LogDataType.TEXT, source);
+ File stderrFile =
+ RemoteFileUtil.fetchRemoteFile(
+ info,
+ options,
+ runUtil,
+ PULL_RESULT_TIMEOUT,
+ mRemoteTradefedDir + STDERR_FILE);
+ if (stderrFile != null) {
+ try (InputStreamSource source = new FileInputStreamSource(stderrFile, true)) {
+ currentInvocationListener.testLog(STDERR_FILE, LogDataType.TEXT, source);
+ }
}
}
- File stderrFile =
- RemoteFileUtil.fetchRemoteFile(
- info,
- options,
- runUtil,
- PULL_RESULT_TIMEOUT,
- mRemoteTradefedDir + STDERR_FILE);
- if (stderrFile != null) {
- try (InputStreamSource source = new FileInputStreamSource(stderrFile, true)) {
- currentInvocationListener.testLog(STDERR_FILE, LogDataType.TEXT, source);
- }
+ // If not result in progress are reported, parse the full results at the end.
+ if (!config.getCommandOptions().shouldReportModuleProgression()) {
+ fetchAndProcessResults(
+ stillRunning,
+ currentInvocationListener,
+ context,
+ info,
+ options,
+ runUtil,
+ mRemoteTradefedDir);
}
-
- fetchAndProcessResults(
- stillRunning,
- currentInvocationListener,
- context,
- info,
- options,
- runUtil,
- mRemoteTradefedDir);
}
private boolean isStillRunning(
@@ -398,7 +413,9 @@
GceAvdInfo info,
TestDeviceOptions options,
IRunUtil runUtil,
- IConfiguration config) {
+ IConfiguration config,
+ IInvocationContext context)
+ throws IOException {
long maxTimeout = config.getCommandOptions().getInvocationTimeout();
Long endTime = null;
if (maxTimeout > 0L) {
@@ -406,7 +423,31 @@
}
boolean stillRunning = true;
int errorConnectCount = 0;
+ int currentIndex = 0;
+ ProtoResultParser parser =
+ new ProtoResultParser(
+ currentInvocationListener,
+ context, /* Don't report invocation level */
+ false,
+ "remote-");
while (stillRunning) {
+ if (config.getCommandOptions().shouldReportModuleProgression()) {
+ File resultFile =
+ RemoteFileUtil.fetchRemoteFile(
+ info,
+ options,
+ runUtil,
+ PULL_RESULT_TIMEOUT,
+ mRemoteTradefedDir + PROTO_RESULT_NAME + currentIndex);
+ if (resultFile != null) {
+ currentIndex++;
+ parser.processFileProto(resultFile);
+ // Don't sleep in that case since we might have more file to process, this will
+ // sleep next time we don't find a file to process on the remote.
+ continue;
+ }
+ }
+
CommandResult psRes =
GceManager.remoteSshCommandExecution(
info,
@@ -443,6 +484,24 @@
RunUtil.getDefault().sleep(REMOTE_PROCESS_RUNNING_WAIT);
}
}
+
+ File resultFile = null;
+ if (config.getCommandOptions().shouldReportModuleProgression()) {
+ // Process all remaining proto files available
+ do {
+ resultFile =
+ RemoteFileUtil.fetchRemoteFile(
+ info,
+ options,
+ runUtil,
+ PULL_RESULT_TIMEOUT,
+ mRemoteTradefedDir + PROTO_RESULT_NAME + currentIndex);
+ if (resultFile != null) {
+ currentIndex++;
+ parser.processFileProto(resultFile);
+ }
+ } while (resultFile != null);
+ }
return stillRunning;
}
@@ -520,6 +579,9 @@
// Setup the remote reporting to a proto file
List<ITestInvocationListener> reporters = new ArrayList<>();
FileProtoResultReporter protoReporter = new FileProtoResultReporter();
+ if (config.getCommandOptions().shouldReportModuleProgression()) {
+ protoReporter.setPeriodicWriting(true);
+ }
protoReporter.setFileOutput(new File(resultDirPath + PROTO_RESULT_NAME));
reporters.add(protoReporter);
@@ -535,7 +597,11 @@
// Dump and log the configuration
File configFile = FileUtil.createTempFile(config.getName(), ".xml");
- config.dumpXml(new PrintWriter(configFile));
+ config.dumpXml(
+ new PrintWriter(configFile),
+ new ArrayList<String>(),
+ /* print deprecated */ true,
+ /* print unchanged*/ false);
try (InputStreamSource source = new FileInputStreamSource(configFile)) {
logger.testLog(REMOTE_CONFIG, LogDataType.XML, source);
}
diff --git a/src/com/android/tradefed/invoker/ShardListener.java b/src/com/android/tradefed/invoker/ShardListener.java
index b3d8348..5805a3b 100644
--- a/src/com/android/tradefed/invoker/ShardListener.java
+++ b/src/com/android/tradefed/invoker/ShardListener.java
@@ -43,6 +43,7 @@
public class ShardListener extends CollectingTestListener {
private ITestInvocationListener mMasterListener;
+ private IInvocationContext mModuleContext = null;
/**
* Create a {@link ShardListener}.
@@ -112,10 +113,9 @@
/** {@inheritDoc} */
@Override
- public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
- super.testRunEnded(elapsedTime, runMetrics);
- CLog.logAndDisplay(LogLevel.INFO, "Sharded test completed: %s",
- getCurrentRunResults().getName());
+ public void testModuleStarted(IInvocationContext moduleContext) {
+ super.testModuleStarted(moduleContext);
+ mModuleContext = moduleContext;
}
/**
@@ -128,16 +128,34 @@
getCurrentRunResults().getName(), failureMessage);
}
- /**
- * {@inheritDoc}
- */
+ /** {@inheritDoc} */
@Override
- public void invocationEnded(long elapsedTime) {
- super.invocationEnded(elapsedTime);
+ public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
+ super.testRunEnded(elapsedTime, runMetrics);
+ CLog.logAndDisplay(
+ LogLevel.INFO, "Sharded test completed: %s", getCurrentRunResults().getName());
+ if (mModuleContext == null) {
+ // testRunEnded only forwards if it's not part of a module. If it's a module
+ // testModuleEnded is in charge of forwarding all run results.
+ synchronized (mMasterListener) {
+ forwardRunResults(getCurrentRunResults());
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void testModuleEnded() {
+ super.testModuleEnded();
+
synchronized (mMasterListener) {
- logShardContent(getMergedTestRunResults());
IInvocationContext moduleContext = null;
+ // TODO: Support attempts and retries
for (TestRunResult runResult : getMergedTestRunResults()) {
+ // Only consider run results of the module in progress
+ if (getModuleContextForRunResult(runResult.getName()) != mModuleContext) {
+ continue;
+ }
// Stop or start the module
if (moduleContext != null
@@ -151,33 +169,44 @@
moduleContext = getModuleContextForRunResult(runResult.getName());
mMasterListener.testModuleStarted(moduleContext);
}
-
- mMasterListener.testRunStarted(
- runResult.getName(), runResult.getExpectedTestCount());
- forwardTestResults(runResult.getTestResults());
- if (runResult.isRunFailure()) {
- mMasterListener.testRunFailed(runResult.getRunFailureMessage());
- }
-
- // Provide a strong association of the run to its logs.
- forwardLogAssociation(runResult.getRunLoggedFiles(), mMasterListener);
-
- mMasterListener.testRunEnded(
- runResult.getElapsedTime(), runResult.getRunProtoMetrics());
+ // Forward the run level results
+ forwardRunResults(runResult);
}
// Close the last module
if (moduleContext != null) {
mMasterListener.testModuleEnded();
moduleContext = null;
}
- // In case there was no run, we still want to report the logs we received.
- if (getMergedTestRunResults().isEmpty()) {
- forwardLogAssociation(getCurrentRunResults().getRunLoggedFiles(), mMasterListener);
- }
+ }
+ mModuleContext = null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void invocationEnded(long elapsedTime) {
+ super.invocationEnded(elapsedTime);
+ synchronized (mMasterListener) {
+ logShardContent(getMergedTestRunResults());
+ // Report all logs not associated with test runs
+ forwardLogAssociation(getNonAssociatedLogFiles(), mMasterListener);
mMasterListener.invocationEnded(elapsedTime);
}
}
+ private void forwardRunResults(TestRunResult runResult) {
+ // TODO: Support attempts and retries
+ mMasterListener.testRunStarted(runResult.getName(), runResult.getExpectedTestCount());
+ forwardTestResults(runResult.getTestResults());
+ if (runResult.isRunFailure()) {
+ mMasterListener.testRunFailed(runResult.getRunFailureMessage());
+ }
+
+ // Provide a strong association of the run to its logs.
+ forwardLogAssociation(runResult.getRunLoggedFiles(), mMasterListener);
+
+ mMasterListener.testRunEnded(runResult.getElapsedTime(), runResult.getRunProtoMetrics());
+ }
+
private void forwardTestResults(Map<TestDescription, TestResult> testResults) {
for (Map.Entry<TestDescription, TestResult> testEntry : testResults.entrySet()) {
mMasterListener.testStarted(testEntry.getKey(), testEntry.getValue().getStartTime());
diff --git a/src/com/android/tradefed/invoker/TestInvocation.java b/src/com/android/tradefed/invoker/TestInvocation.java
index b7b2599..8f18c26 100644
--- a/src/com/android/tradefed/invoker/TestInvocation.java
+++ b/src/com/android/tradefed/invoker/TestInvocation.java
@@ -32,6 +32,7 @@
import com.android.tradefed.device.cloud.NestedRemoteDevice;
import com.android.tradefed.device.cloud.RemoteAndroidVirtualDevice;
import com.android.tradefed.guice.InvocationScope;
+import com.android.tradefed.invoker.logger.InvocationMetricLogger;
import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecution;
import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
import com.android.tradefed.invoker.shard.ShardBuildCloner;
@@ -55,6 +56,7 @@
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.IResumableTest;
import com.android.tradefed.testtype.IRetriableTest;
+import com.android.tradefed.testtype.retry.ResultAggregator;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.PrettyPrintDelimiter;
@@ -91,7 +93,8 @@
*/
private static final String BATTERY_ATTRIBUTE_FORMAT_KEY = "%s-battery-%s";
- static final String TRADEFED_LOG_NAME = "host_log";
+ public static final String TRADEFED_LOG_NAME = "host_log";
+ public static final String TRADEFED_END_HOST_LOG = "end_host_log";
/** Suffix used on host_log for the part before sharding occurs. */
static final String BEFORE_SHARDING_SUFFIX = "_before_sharding";
static final String DEVICE_LOG_NAME_PREFIX = "device_logcat_";
@@ -339,20 +342,30 @@
invocationPath.reportLogs(device, listener, Stage.TEARDOWN);
}
if (mStopRequested) {
- CLog.e(
- "====================================================================="
- + "====");
- CLog.e(
+ String message =
"Invocation was interrupted due to TradeFed stop, results will be "
- + "affected.");
- CLog.e(
- "====================================================================="
- + "====");
+ + "affected.";
+ listener.invocationFailed(new RuntimeException(message));
+ PrettyPrintDelimiter.printStageDelimiter(message);
}
reportHostLog(listener, config);
+
elapsedTime = System.currentTimeMillis() - startTime;
if (!resumed) {
- listener.invocationEnded(elapsedTime);
+ // Init a log for the end of the host_log.
+ ILeveledLogOutput endHostLog = config.getLogOutput();
+ endHostLog.init();
+ getLogRegistry().registerLogger(endHostLog);
+ PrettyPrintDelimiter.printStageDelimiter("===== Result Reporters =====");
+ try {
+ // Copy the invocation metrics to the context
+ ((InvocationContext) context).logInvocationMetrics();
+ listener.invocationEnded(elapsedTime);
+ } finally {
+ InvocationMetricLogger.clearInvocationMetrics();
+ endHostLog.closeLog();
+ getLogRegistry().unregisterLogger();
+ }
}
} finally {
invocationPath.cleanUpBuilds(context, config);
@@ -620,6 +633,17 @@
allListeners.addAll(config.getTestInvocationListeners());
allListeners.addAll(Arrays.asList(extraListeners));
ITestInvocationListener listener = null;
+
+ // Auto retry feature
+ if (config.getCommandOptions().isAutoRetryEnabled()
+ && config.getCommandOptions().getMaxRetryCount() > 1) {
+ CLog.d("Auto-retry enabled, using the ResultAggregator to handle multiple retries.");
+ ResultAggregator aggregator =
+ new ResultAggregator(
+ allListeners, config.getCommandOptions().getRetryStrategy());
+ allListeners = Arrays.asList(aggregator);
+ }
+
if (!config.getPostProcessors().isEmpty()) {
ITestInvocationListener forwarder = new ResultAndLogForwarder(allListeners);
// Post-processors are the first layer around the final reporters.
@@ -654,13 +678,16 @@
scope.seed(IRescheduler.class, rescheduler);
scope.seedConfiguration(config);
try {
- mStatus = "fetching build";
ILeveledLogOutput leveledLogOutput = config.getLogOutput();
leveledLogOutput.init();
if (leveledLogOutput instanceof BaseLeveledLogOutput) {
((BaseLeveledLogOutput) leveledLogOutput).initFilters(config);
}
- getLogRegistry().registerLogger(config.getLogOutput());
+ getLogRegistry().registerLogger(leveledLogOutput);
+ mStatus = "resolving dynamic options";
+ config.resolveDynamicOptions();
+
+ mStatus = "fetching build";
for (String deviceName : context.getDeviceConfigNames()) {
context.getDevice(deviceName).clearLastConnectedWifiNetwork();
context.getDevice(deviceName)
@@ -712,7 +739,7 @@
CLog.e(e);
setExitCode(ExitCode.THROWABLE_EXCEPTION, e);
try {
- invocationPath.runDevicePostInvocationTearDown(context, config);
+ invocationPath.runDevicePostInvocationTearDown(context, config, e);
} finally {
listener.invocationFailed(e);
// Reports the logs
@@ -726,7 +753,8 @@
}
}
- boolean sharding = invocationPath.shardConfig(config, context, rescheduler);
+ boolean sharding =
+ invocationPath.shardConfig(config, context, rescheduler, listener);
if (sharding) {
CLog.i(
"Invocation for %s has been sharded, rescheduling",
@@ -745,7 +773,7 @@
CLog.e("No tests to run");
if (deviceInit) {
// If we did an early setup, do the tear down.
- invocationPath.runDevicePostInvocationTearDown(context, config);
+ invocationPath.runDevicePostInvocationTearDown(context, config, null);
}
listener.invocationEnded(0L);
return;
@@ -838,19 +866,20 @@
return;
}
try {
- if (log != null && !FileUtil.readStringFromFile(log).isEmpty()) {
- try (InputStreamSource source = new FileInputStreamSource(log)) {
- logger.testLog(
- String.format(
- "executeShellCommandLog_%s", device.getSerialNumber()),
- LogDataType.TEXT,
- source);
- }
+ if (FileUtil.readStringFromFile(log).isEmpty()) {
+ CLog.d("executeShellCommandLog file was empty, skip logging.");
+ return;
}
} catch (IOException e) {
// Ignored
CLog.e(e);
}
+ try (InputStreamSource source = new FileInputStreamSource(log)) {
+ logger.testLog(
+ String.format("executeShellCommandLog_%s", device.getSerialNumber()),
+ LogDataType.TEXT,
+ source);
+ }
}
}
}
diff --git a/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java b/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
new file mode 100644
index 0000000..bf2eb87
--- /dev/null
+++ b/src/com/android/tradefed/invoker/logger/InvocationMetricLogger.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.invoker.logger;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/** A utility class for an invocation to log some metrics. */
+public class InvocationMetricLogger {
+
+ /** Some special named key that we will always populate for the invocation. */
+ public enum InvocationMetricKey {
+ FETCH_BUILD("fetch_build_time_ms"),
+ SETUP("setup_time_ms");
+
+ private final String mKeyName;
+
+ private InvocationMetricKey(String key) {
+ mKeyName = key;
+ }
+
+ @Override
+ public String toString() {
+ return mKeyName;
+ }
+ }
+
+ private InvocationMetricLogger() {}
+
+ /**
+ * Track metrics per ThreadGroup as a proxy to invocation since an invocation run within one
+ * threadgroup.
+ */
+ private static final Map<ThreadGroup, Map<String, String>> mPerGroupMetrics =
+ Collections.synchronizedMap(new HashMap<ThreadGroup, Map<String, String>>());
+
+ /**
+ * Add one key-value to be tracked at the invocation level.
+ *
+ * @param key The key under which the invocation metric will be tracked.
+ * @param value The value of the invocation metric.
+ */
+ public static void addInvocationMetrics(InvocationMetricKey key, String value) {
+ addInvocationMetrics(key.toString(), value);
+ }
+
+ /**
+ * Add one key-value to be tracked at the invocation level. Don't expose the String key yet to
+ * avoid abuse, stick to the official {@link InvocationMetricKey} to start with.
+ *
+ * @param key The key under which the invocation metric will be tracked.
+ * @param value The value of the invocation metric.
+ */
+ private static void addInvocationMetrics(String key, String value) {
+ ThreadGroup group = Thread.currentThread().getThreadGroup();
+ synchronized (mPerGroupMetrics) {
+ if (mPerGroupMetrics.get(group) == null) {
+ mPerGroupMetrics.put(group, new HashMap<>());
+ }
+ mPerGroupMetrics.get(group).put(key, value);
+ }
+ }
+
+ /** Returns the Map of invocation metrics for the invocation in progress. */
+ public static Map<String, String> getInvocationMetrics() {
+ ThreadGroup group = Thread.currentThread().getThreadGroup();
+ synchronized (mPerGroupMetrics) {
+ if (mPerGroupMetrics.get(group) == null) {
+ mPerGroupMetrics.put(group, new HashMap<>());
+ }
+ }
+ return new HashMap<>(mPerGroupMetrics.get(group));
+ }
+
+ /** Clear the invocation metrics for an invocation. */
+ public static void clearInvocationMetrics() {
+ ThreadGroup group = Thread.currentThread().getThreadGroup();
+ mPerGroupMetrics.remove(group);
+ }
+}
diff --git a/src/com/android/tradefed/invoker/shard/IShardHelper.java b/src/com/android/tradefed/invoker/shard/IShardHelper.java
index 37d0928..7eb3079 100644
--- a/src/com/android/tradefed/invoker/shard/IShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/IShardHelper.java
@@ -18,6 +18,7 @@
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.IRescheduler;
+import com.android.tradefed.log.ITestLogger;
/** Interface of an object that describes the sharding strategy to adopt for a configuration. */
public interface IShardHelper {
@@ -31,5 +32,8 @@
* @return True if the configuration was sharded. false otherwise.
*/
public boolean shardConfig(
- IConfiguration config, IInvocationContext context, IRescheduler rescheduler);
+ IConfiguration config,
+ IInvocationContext context,
+ IRescheduler rescheduler,
+ ITestLogger logger);
}
diff --git a/src/com/android/tradefed/invoker/shard/ShardHelper.java b/src/com/android/tradefed/invoker/shard/ShardHelper.java
index 4aa1c06..b0935ee 100644
--- a/src/com/android/tradefed/invoker/shard/ShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/ShardHelper.java
@@ -28,9 +28,11 @@
import com.android.tradefed.invoker.ShardListener;
import com.android.tradefed.invoker.ShardMasterResultForwarder;
import com.android.tradefed.invoker.shard.token.ITokenRequest;
+import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.IShardableListener;
import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.ITestLoggerReceiver;
import com.android.tradefed.suite.checker.ISystemStatusChecker;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.testtype.IDeviceTest;
@@ -85,12 +87,15 @@
*/
@Override
public boolean shardConfig(
- IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
+ IConfiguration config,
+ IInvocationContext context,
+ IRescheduler rescheduler,
+ ITestLogger logger) {
List<IRemoteTest> shardableTests = new ArrayList<IRemoteTest>();
boolean isSharded = false;
Integer shardCount = config.getCommandOptions().getShardCount();
for (IRemoteTest test : config.getTests()) {
- isSharded |= shardTest(shardableTests, test, shardCount, context);
+ isSharded |= shardTest(shardableTests, test, shardCount, context, logger);
}
if (!isSharded) {
return false;
@@ -193,10 +198,11 @@
return GlobalConfiguration.getInstance();
}
- /** Runs the {@link IConfiguration#validateOptions(boolean)} on the config. */
+ /** Runs the {@link IConfiguration#validateOptions()} on the config. */
@VisibleForTesting
protected void validateOptions(IConfiguration config) throws ConfigurationException {
- config.validateOptions(true);
+ config.validateOptions();
+ config.resolveDynamicOptions();
}
/**
@@ -251,7 +257,8 @@
List<IRemoteTest> shardableTests,
IRemoteTest test,
Integer shardCount,
- IInvocationContext context) {
+ IInvocationContext context,
+ ITestLogger logger) {
boolean isSharded = false;
if (test instanceof IShardableTest) {
// inject device and build since they might be required to shard.
@@ -267,6 +274,9 @@
if (test instanceof IInvocationContextReceiver) {
((IInvocationContextReceiver) test).setInvocationContext(context);
}
+ if (test instanceof ITestLoggerReceiver) {
+ ((ITestLoggerReceiver) test).setTestLogger(logger);
+ }
IShardableTest shardableTest = (IShardableTest) test;
Collection<IRemoteTest> shards = null;
diff --git a/src/com/android/tradefed/invoker/shard/StrictShardHelper.java b/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
index 23021b0..4be515c 100644
--- a/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
+++ b/src/com/android/tradefed/invoker/shard/StrictShardHelper.java
@@ -19,7 +19,9 @@
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.IRescheduler;
+import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestLoggerReceiver;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IInvocationContextReceiver;
@@ -42,19 +44,22 @@
/** {@inheritDoc} */
@Override
public boolean shardConfig(
- IConfiguration config, IInvocationContext context, IRescheduler rescheduler) {
+ IConfiguration config,
+ IInvocationContext context,
+ IRescheduler rescheduler,
+ ITestLogger logger) {
Integer shardCount = config.getCommandOptions().getShardCount();
Integer shardIndex = config.getCommandOptions().getShardIndex();
if (shardIndex == null) {
- return super.shardConfig(config, context, rescheduler);
+ return super.shardConfig(config, context, rescheduler, logger);
}
if (shardCount == null) {
throw new RuntimeException("shard-count is null while shard-index is " + shardIndex);
}
// Split tests in place, without actually sharding.
- List<IRemoteTest> listAllTests = getAllTests(config, shardCount, context);
+ List<IRemoteTest> listAllTests = getAllTests(config, shardCount, context, logger);
// We cannot shuffle to get better average results
normalizeDistribution(listAllTests, shardCount);
List<IRemoteTest> splitList;
@@ -78,7 +83,10 @@
* @return the list of all {@link IRemoteTest}.
*/
private List<IRemoteTest> getAllTests(
- IConfiguration config, Integer shardCount, IInvocationContext context) {
+ IConfiguration config,
+ Integer shardCount,
+ IInvocationContext context,
+ ITestLogger logger) {
List<IRemoteTest> allTests = new ArrayList<>();
for (IRemoteTest test : config.getTests()) {
if (test instanceof IShardableTest) {
@@ -95,6 +103,9 @@
if (test instanceof IInvocationContextReceiver) {
((IInvocationContextReceiver) test).setInvocationContext(context);
}
+ if (test instanceof ITestLoggerReceiver) {
+ ((ITestLoggerReceiver) test).setTestLogger(logger);
+ }
// Handling of the ITestSuite is a special case, we do not allow pool of tests
// since each shard needs to be independent.
diff --git a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
index b341d87..85c0c28 100644
--- a/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
+++ b/src/com/android/tradefed/invoker/shard/TestsPoolPoller.java
@@ -214,7 +214,8 @@
// At this point only the <test> object needs to be validated for options, this
// ensures that the object is fine before running it.
validationConfig.setTest(test);
- validationConfig.validateOptions(true);
+ validationConfig.validateOptions();
+ validationConfig.resolveDynamicOptions();
// Run the test itself and prevent random exception from stopping the poller.
if (test instanceof IMetricCollectorReceiver) {
((IMetricCollectorReceiver) test).setMetricCollectors(mCollectors);
diff --git a/src/com/android/tradefed/log/BaseStreamLogger.java b/src/com/android/tradefed/log/BaseStreamLogger.java
new file mode 100644
index 0000000..5027386
--- /dev/null
+++ b/src/com/android/tradefed/log/BaseStreamLogger.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.log;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.util.StreamUtil;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/** A {@link ILeveledLogOutput} that directs log messages to an output stream and to stdout. */
+public abstract class BaseStreamLogger<OS extends OutputStream> extends BaseLeveledLogOutput {
+
+ /**
+ * Map of log tag to a level they are forced at for writing to log file purpose. This ensure
+ * that some logs we have less control over can still be regulated.
+ */
+ private static final Map<String, LogLevel> FORCED_LOG_LEVEL = new HashMap<>();
+
+ static {
+ FORCED_LOG_LEVEL.put("ddms", LogLevel.WARN);
+ }
+
+ @Option(name = "log-level", description = "the minimum log level to log.")
+ private LogLevel mLogLevel = LogLevel.DEBUG;
+
+ @Option(
+ name = "log-level-display",
+ shortName = 'l',
+ description = "the minimum log level to display on stdout.",
+ importance = Importance.ALWAYS
+ )
+ private LogLevel mLogLevelDisplay = LogLevel.ERROR;
+
+ // output stream to print logs to, exposed to subclasses
+ protected OS mOutputStream;
+
+ @Override
+ public LogLevel getLogLevel() {
+ return mLogLevel;
+ }
+
+ @Override
+ public void setLogLevel(LogLevel logLevel) {
+ mLogLevel = logLevel;
+ }
+
+ /** Sets the minimum {@link LogLevel} to display on stdout. */
+ public void setLogLevelDisplay(LogLevel logLevel) {
+ mLogLevelDisplay = logLevel;
+ }
+
+ /** @return current minimum {@link LogLevel} to display on stdout. */
+ LogLevel getLogLevelDisplay() {
+ return mLogLevelDisplay;
+ }
+
+ @Override
+ public void closeLog() {
+ StreamUtil.flushAndCloseStream(mOutputStream);
+ mOutputStream = null;
+ }
+
+ @Override
+ public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
+ internalPrintLog(logLevel, tag, message, true /* force print to stdout */);
+ }
+
+ @Override
+ public void printLog(LogLevel logLevel, String tag, String message) {
+ internalPrintLog(logLevel, tag, message, false /* don't force stdout */);
+ }
+
+ /**
+ * A version of printLog(...) which can be forced to print to stdout, even if the log level
+ * isn't above the urgency threshold.
+ */
+ private void internalPrintLog(
+ LogLevel logLevel, String tag, String message, boolean forceStdout) {
+ String outMessage = LogUtil.getLogFormatString(logLevel, tag, message);
+ if (shouldDisplay(forceStdout, mLogLevelDisplay, logLevel, tag)) {
+ System.out.print(outMessage);
+ }
+ if (shouldWrite(tag, logLevel, mLogLevel)) {
+ try {
+ writeToLog(outMessage);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // Determines whether a message should be written to the output stream.
+ private boolean shouldWrite(String tag, LogLevel messageLogLevel, LogLevel invocationLogLevel) {
+ LogLevel forcedLevel = FORCED_LOG_LEVEL.get(tag);
+ if (forcedLevel == null) {
+ return true;
+ }
+ // Use the highest level of our forced and invocation to decide if we should log the
+ // particular tag.
+ int minWriteLevel = Math.max(forcedLevel.getPriority(), invocationLogLevel.getPriority());
+ return messageLogLevel.getPriority() >= minWriteLevel;
+ }
+
+ /**
+ * Writes a message to the output stream.
+ *
+ * @param message the entry to write to log
+ * @throws IOException if an I/O error occurs
+ */
+ protected void writeToLog(String message) throws IOException {
+ if (mOutputStream != null) {
+ mOutputStream.write(message.getBytes());
+ }
+ }
+}
diff --git a/src/com/android/tradefed/log/FileLogger.java b/src/com/android/tradefed/log/FileLogger.java
index 100f5ca..c985a0d 100644
--- a/src/com/android/tradefed/log/FileLogger.java
+++ b/src/com/android/tradefed/log/FileLogger.java
@@ -15,9 +15,7 @@
*/
package com.android.tradefed.log;
-import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.config.Option;
-import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.result.ByteArrayInputStreamSource;
@@ -26,46 +24,20 @@
import com.android.tradefed.util.SizeLimitedOutputStream;
import com.android.tradefed.util.StreamUtil;
+import com.google.common.annotations.VisibleForTesting;
+
import java.io.IOException;
import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
/** A {@link ILeveledLogOutput} that directs log messages to a file and to stdout. */
@OptionClass(alias = "file")
-public class FileLogger extends BaseLeveledLogOutput {
+public class FileLogger extends BaseStreamLogger<SizeLimitedOutputStream> {
private static final String TEMP_FILE_PREFIX = "tradefed_log_";
private static final String TEMP_FILE_SUFFIX = ".txt";
- /**
- * Map of log tag to a level they are forced at for writing to log file purpose. This ensure
- * that some logs we have less control over can still be regulated.
- */
- private static final Map<String, LogLevel> FORCED_LOG_LEVEL = new HashMap<>();
-
- static {
- FORCED_LOG_LEVEL.put("ddms", LogLevel.WARN);
- }
-
- @Option(name = "log-level", description = "the minimum log level to log.")
- private LogLevel mLogLevel = LogLevel.DEBUG;
-
- @Option(name = "log-level-display", shortName = 'l',
- description = "the minimum log level to display on stdout.",
- importance = Importance.ALWAYS)
- private LogLevel mLogLevelDisplay = LogLevel.ERROR;
-
@Option(name = "max-log-size", description = "maximum allowable size of tmp log data in mB.")
private long mMaxLogSizeMbytes = 20;
- private SizeLimitedOutputStream mLogStream;
-
- public FileLogger() {
- }
-
- /**
- * {@inheritDoc}
- */
@Override
public void init() throws IOException {
init(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
@@ -78,7 +50,7 @@
* @param fileSuffix the extension of the file where to log.
*/
protected void init(String logPrefix, String fileSuffix) {
- mLogStream =
+ mOutputStream =
new SizeLimitedOutputStream(mMaxLogSizeMbytes * 1024 * 1024, logPrefix, fileSuffix);
}
@@ -95,104 +67,18 @@
return logger;
}
- /**
- * {@inheritDoc}
- */
- @Override
- public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
- internalPrintLog(logLevel, tag, message, true /* force print to stdout */);
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void printLog(LogLevel logLevel, String tag, String message) {
- internalPrintLog(logLevel, tag, message, false /* don't force stdout */);
- }
-
- /**
- * A version of printLog(...) which can be forced to print to stdout, even if the log level
- * isn't above the urgency threshold.
- */
- private void internalPrintLog(LogLevel logLevel, String tag, String message,
- boolean forceStdout) {
- String outMessage = LogUtil.getLogFormatString(logLevel, tag, message);
- if (shouldDisplay(forceStdout, mLogLevelDisplay, logLevel, tag)) {
- System.out.print(outMessage);
- }
- try {
- if (shouldWrite(tag, logLevel, mLogLevel)) {
- writeToLog(outMessage);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- /**
- * Writes given message to log.
- * <p/>
- * Exposed for unit testing.
- *
- * @param outMessage the entry to write to log
- * @throws IOException
- */
- void writeToLog(String outMessage) throws IOException {
- if (mLogStream != null) {
- mLogStream.write(outMessage.getBytes());
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public LogLevel getLogLevel() {
- return mLogLevel;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void setLogLevel(LogLevel logLevel) {
- mLogLevel = logLevel;
- }
-
- /**
- * Sets the log level filtering for stdout.
- *
- * @param logLevel the minimum {@link LogLevel} to display
- */
- public void setLogLevelDisplay(LogLevel logLevel) {
- mLogLevelDisplay = logLevel;
- }
-
- /**
- * Gets the log level filtering for stdout.
- *
- * @return the current {@link LogLevel}
- */
- LogLevel getLogLevelDisplay() {
- return mLogLevelDisplay;
- }
-
/** Returns the max log size of the log in MBytes. */
public long getMaxLogSizeMbytes() {
return mMaxLogSizeMbytes;
}
- /**
- * {@inheritDoc}
- */
@Override
public InputStreamSource getLog() {
- if (mLogStream != null) {
+ if (mOutputStream != null) {
try {
// create a InputStream from log file
- mLogStream.flush();
- return new SnapshotInputStreamSource("FileLogger", mLogStream.getData());
+ mOutputStream.flush();
+ return new SnapshotInputStreamSource("FileLogger", mOutputStream.getData());
} catch (IOException e) {
System.err.println("Failed to get log");
e.printStackTrace();
@@ -201,22 +87,16 @@
return new ByteArrayInputStreamSource(new byte[0]);
}
- /**
- * {@inheritDoc}
- */
@Override
public void closeLog() {
doCloseLog();
}
- /**
- * Flushes stream and closes log file.
- * <p/>
- * Exposed for unit testing.
- */
+ /** Flushes stream and closes log file. */
+ @VisibleForTesting
void doCloseLog() {
- SizeLimitedOutputStream stream = mLogStream;
- mLogStream = null;
+ SizeLimitedOutputStream stream = mOutputStream;
+ mOutputStream = null;
StreamUtil.flushAndCloseStream(stream);
if (stream != null) {
stream.delete();
@@ -226,26 +106,12 @@
/**
* Dump the contents of the input stream to this log
*
- * @param inputStream
- * @throws IOException
+ * @param inputStream input stream to dump
+ * @throws IOException if an I/O error occurs
*/
void dumpToLog(InputStream inputStream) throws IOException {
- if (mLogStream != null) {
- StreamUtil.copyStreams(inputStream, mLogStream);
+ if (mOutputStream != null) {
+ StreamUtil.copyStreams(inputStream, mOutputStream);
}
}
-
- private boolean shouldWrite(String tag, LogLevel messageLogLevel, LogLevel invocationLogLevel) {
- LogLevel forcedLevel = FORCED_LOG_LEVEL.get(tag);
- if (forcedLevel == null) {
- return true;
- }
- // Use the highest level of our forced and invocation to decide if we should log the
- // particular tag.
- int minWriteLevel = Math.max(forcedLevel.getPriority(), invocationLogLevel.getPriority());
- if (messageLogLevel.getPriority() >= minWriteLevel) {
- return true;
- }
- return false;
- }
}
diff --git a/src/com/android/tradefed/log/HistoryLogger.java b/src/com/android/tradefed/log/HistoryLogger.java
index be983c7..d713163 100644
--- a/src/com/android/tradefed/log/HistoryLogger.java
+++ b/src/com/android/tradefed/log/HistoryLogger.java
@@ -16,6 +16,7 @@
package com.android.tradefed.log;
import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.OptionCopier;
import com.android.tradefed.log.ILogRegistry.EventType;
import org.json.JSONObject;
@@ -37,14 +38,12 @@
init(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
}
- /** {@inheritDoc} */
@Override
public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
throw new UnsupportedOperationException(
"printAndPromptLog is not supported by HistoryLogger");
}
- /** {@inheritDoc} */
@Override
public void printLog(LogLevel logLevel, String tag, String message) {
throw new UnsupportedOperationException("printLog is not supported by HistoryLogger");
@@ -84,9 +83,8 @@
@Override
public ILeveledLogOutput clone() {
- FileLogger logger = new HistoryLogger();
- logger.setLogLevelDisplay(getLogLevelDisplay());
- logger.setLogLevel(getLogLevel());
+ HistoryLogger logger = new HistoryLogger();
+ OptionCopier.copyOptionsNoThrow(this, logger);
return logger;
}
}
diff --git a/src/com/android/tradefed/log/ILogRegistry.java b/src/com/android/tradefed/log/ILogRegistry.java
index 4c2cd55..860890a 100644
--- a/src/com/android/tradefed/log/ILogRegistry.java
+++ b/src/com/android/tradefed/log/ILogRegistry.java
@@ -19,7 +19,6 @@
import com.android.ddmlib.Log.ILogOutput;
import com.android.ddmlib.Log.LogLevel;
-import java.util.List;
import java.util.Map;
/**
@@ -97,11 +96,4 @@
/** Diagnosis method to dump all logs to files. */
public void dumpLogs();
- /**
- * Get log entries after the given logEntry.
- *
- * @param logEntry a {@link LogEntry} as a pivot.
- * @return log entries logged after the given logEntry.
- */
- public List<LogEntry> getLogEntriesAfter(LogEntry logEntry);
}
diff --git a/src/com/android/tradefed/log/LogEntry.java b/src/com/android/tradefed/log/LogEntry.java
deleted file mode 100644
index 99aed8c..0000000
--- a/src/com/android/tradefed/log/LogEntry.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.log;
-
-import com.android.ddmlib.Log.LogLevel;
-
-import java.util.concurrent.atomic.AtomicLong;
-
-/** A log entry store's information for one log. */
-public class LogEntry implements Comparable<LogEntry> {
-
- private static final AtomicLong sLogIndex = new AtomicLong(0);
- private final long mTimestamp; // time in milliseconds.
- private final LogLevel mLogLevel;
- private final String mTag;
- private final String mMessage;
- private final long mLogIndex;
-
- /**
- * Constructor for LogEntry.
- *
- * @param timestamp the currentTimeMillis when the log happen.
- * @param logLevel log level of the log.
- * @param tag log's tag.
- * @param message the log's actual message.
- */
- public LogEntry(long timestamp, LogLevel logLevel, String tag, String message) {
- mTimestamp = timestamp;
- mLogLevel = logLevel;
- mTag = tag;
- mMessage = message;
- mLogIndex = sLogIndex.incrementAndGet();
- }
-
- public LogEntry(LogLevel logLevel, String tag, String message) {
- this(System.currentTimeMillis(), logLevel, tag, message);
- }
-
- public long getTimestamp() {
- return mTimestamp;
- }
-
- public LogLevel getLogLevel() {
- return mLogLevel;
- }
-
- public String getTag() {
- return mTag;
- }
-
- public String getMessage() {
- return mMessage;
- }
-
- @Override
- public int compareTo(LogEntry o) {
- if (this.mLogIndex > o.mLogIndex) {
- return 1;
- }
- if (this.mLogIndex == o.mLogIndex) {
- return 0;
- }
- return -1;
- }
-}
diff --git a/src/com/android/tradefed/log/LogRegistry.java b/src/com/android/tradefed/log/LogRegistry.java
index b6c04a1..2acb8eb 100644
--- a/src/com/android/tradefed/log/LogRegistry.java
+++ b/src/com/android/tradefed/log/LogRegistry.java
@@ -20,20 +20,15 @@
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.util.FileUtil;
-import com.google.common.annotations.VisibleForTesting;
-
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
-import java.util.List;
import java.util.Map;
-import java.util.concurrent.ConcurrentLinkedQueue;
/**
* A {@link ILogRegistry} implementation that multiplexes and manages different loggers,
@@ -48,12 +43,10 @@
private static final String LOG_TAG = "LogRegistry";
private static final String GLOBAL_LOG_PREFIX = "tradefed_global_log_";
private static final String HISTORY_LOG_PREFIX = "tradefed_history_log_";
- private static final long MAX_HISTORY_SIZE = 10000;
private static LogRegistry mLogRegistry = null;
private Map<ThreadGroup, ILeveledLogOutput> mLogTable = new Hashtable<>();
private FileLogger mGlobalLogger;
private HistoryLogger mHistoryLogger;
- private ConcurrentLinkedQueue<LogEntry> mLogEntryHistory = new ConcurrentLinkedQueue<>();
/**
* Package-private constructor; callers should use {@link #getLogRegistry} to get an instance of
@@ -162,32 +155,6 @@
return Thread.currentThread().getThreadGroup();
}
- private void addToLogEntryHistory(LogLevel logLevel, String tag, String message) {
- mLogEntryHistory.add(new LogEntry(logLevel, tag, message));
- while (mLogEntryHistory.size() > getMaxHistorySize()) {
- mLogEntryHistory.poll();
- }
- }
-
- @VisibleForTesting
- long getMaxHistorySize() {
- return MAX_HISTORY_SIZE;
- }
-
- /** {@inheritDoc} */
- @Override
- public List<LogEntry> getLogEntriesAfter(LogEntry logEntry) {
- List<LogEntry> logs = new ArrayList<LogEntry>(mLogEntryHistory);
- if (logs.isEmpty() || logEntry == null) {
- return logs;
- }
- int ind = logs.indexOf(logEntry);
- if (ind < 0) {
- return logs;
- }
- return logs.subList(ind + 1, logs.size());
- }
-
/**
* {@inheritDoc}
*/
@@ -198,7 +165,6 @@
if (logLevel.getPriority() >= currentLogLevel.getPriority()) {
log.printLog(logLevel, tag, message);
}
- addToLogEntryHistory(logLevel, tag, message);
}
/**
@@ -207,7 +173,6 @@
@Override
public void printAndPromptLog(LogLevel logLevel, String tag, String message) {
getLogger().printAndPromptLog(logLevel, tag, message);
- addToLogEntryHistory(logLevel, tag, message);
}
/**
diff --git a/src/com/android/tradefed/log/SimpleFileLogger.java b/src/com/android/tradefed/log/SimpleFileLogger.java
new file mode 100644
index 0000000..5322b43
--- /dev/null
+++ b/src/com/android/tradefed/log/SimpleFileLogger.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.log;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.config.OptionCopier;
+import com.android.tradefed.result.ByteArrayInputStreamSource;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.InputStreamSource;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/** A {@link ILeveledLogOutput} that directs log messages to stdout and to a single log file. */
+@OptionClass(alias = "simple-file")
+public class SimpleFileLogger extends BaseStreamLogger<FileOutputStream> {
+
+ @Option(name = "path", description = "File path to log to.", mandatory = true)
+ private File mFile;
+
+ @Override
+ public void init() throws IOException {
+ mFile.getParentFile().mkdirs();
+ mOutputStream = new FileOutputStream(mFile, true);
+ }
+
+ @Override
+ public InputStreamSource getLog() {
+ if (mFile != null) {
+ return new FileInputStreamSource(mFile);
+ }
+ return new ByteArrayInputStreamSource(new byte[0]);
+ }
+
+ @Override
+ public SimpleFileLogger clone() {
+ SimpleFileLogger logger = new SimpleFileLogger();
+ OptionCopier.copyOptionsNoThrow(this, logger);
+ return logger;
+ }
+}
diff --git a/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java b/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
index 8ffafe8..f6e4100 100644
--- a/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
+++ b/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
@@ -44,6 +44,7 @@
private static final String STATS_KEY_VAR = "var";
private static final String STATS_KEY_STDEV = "stdev";
private static final String STATS_KEY_MEDIAN = "median";
+ private static final String STATS_KEY_TOTAL = "total";
// Separator for final upload
private static final String STATS_KEY_SEPARATOR = "-";
@@ -186,6 +187,7 @@
stats.put(STATS_KEY_VAR, variance);
stats.put(STATS_KEY_STDEV, Math.sqrt(variance));
stats.put(STATS_KEY_MEDIAN, median);
+ stats.put(STATS_KEY_TOTAL, sum);
return stats;
}
}
diff --git a/src/com/android/tradefed/result/CollectingTestListener.java b/src/com/android/tradefed/result/CollectingTestListener.java
index cd32cbd..aa8a4c2 100644
--- a/src/com/android/tradefed/result/CollectingTestListener.java
+++ b/src/com/android/tradefed/result/CollectingTestListener.java
@@ -21,6 +21,7 @@
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.testtype.retry.MergeStrategy;
import com.google.common.annotations.VisibleForTesting;
@@ -64,6 +65,10 @@
private TestRunResult mCurrentTestRunResult = new TestRunResult();
/** True if the default initialized mCurrentTestRunResult has its original value. */
private boolean mDefaultRun = true;
+ /** Track whether or not a test run is currently in progress */
+ private boolean mRunInProgress = false;
+
+ private Map<String, LogFile> mNonAssociatedLogFiles = new LinkedHashMap<>();
// Tracks if mStatusCounts are accurate, or if they need to be recalculated
private AtomicBoolean mIsCountDirty = new AtomicBoolean(true);
@@ -176,6 +181,12 @@
/** {@inheritDoc} */
@Override
public void testRunStarted(String name, int numTests, int attemptNumber) {
+ testRunStarted(name, numTests, attemptNumber, System.currentTimeMillis());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void testRunStarted(String name, int numTests, int attemptNumber, long startTime) {
setCountDirty();
// Only testRunStarted can affect the expected count.
mIsExpectedCountDirty.set(true);
@@ -205,7 +216,7 @@
int size = results.size();
for (int i = size; i < attemptNumber; i++) {
TestRunResult result = getNewRunResult();
- result.testRunStarted(name, numTests);
+ result.testRunStarted(name, numTests, startTime);
String errorMessage =
String.format(
"Run attempt %s of %s did not exists, but got attempt %s. This is a placeholder for the missing attempt.",
@@ -220,7 +231,8 @@
}
mCurrentTestRunResult = results.get(attemptNumber);
- mCurrentTestRunResult.testRunStarted(name, numTests);
+ mCurrentTestRunResult.testRunStarted(name, numTests, startTime);
+ mRunInProgress = true;
}
/** {@inheritDoc} */
@@ -228,6 +240,7 @@
public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
setCountDirty();
mCurrentTestRunResult.testRunEnded(elapsedTime, runMetrics);
+ mRunInProgress = false;
}
/** {@inheritDoc} */
@@ -292,7 +305,11 @@
/** {@inheritDoc} */
@Override
public void logAssociation(String dataName, LogFile logFile) {
- mCurrentTestRunResult.testLogSaved(dataName, logFile);
+ if (mRunInProgress) {
+ mCurrentTestRunResult.testLogSaved(dataName, logFile);
+ } else {
+ mNonAssociatedLogFiles.put(dataName, logFile);
+ }
}
/**
@@ -459,6 +476,23 @@
}
/**
+ * Gets all the results for a given attempt.
+ *
+ * @param attempt The attempt we want results for.
+ * @return All {@link TestRunResult} for a given attempt.
+ */
+ public List<TestRunResult> getTestRunForAttempts(int attempt) {
+ List<TestRunResult> allResultForAttempts = new ArrayList<>();
+ for (Entry<String, List<TestRunResult>> runInfo : mTestRunResultMap.entrySet()) {
+ if (attempt < runInfo.getValue().size()) {
+ TestRunResult attemptRes = runInfo.getValue().get(attempt);
+ allResultForAttempts.add(attemptRes);
+ }
+ }
+ return allResultForAttempts;
+ }
+
+ /**
* Returns whether a given test run name has any results.
*
* @param testRunName The name given by {{@link #testRunStarted(String, int)}.
@@ -507,4 +541,18 @@
public IInvocationContext getModuleContextForRunResult(String testRunName) {
return mModuleContextMap.get(testRunName);
}
+
+ /** Returns a copy of the map containing all the logged file not associated with a test run. */
+ public Map<String, LogFile> getNonAssociatedLogFiles() {
+ return new LinkedHashMap<>(mNonAssociatedLogFiles);
+ }
+
+ /**
+ * Allows to clear the results for a given run name. Should only be used in some cases like the
+ * aggregator of results.
+ */
+ protected final synchronized void clearResultsForName(String testRunName) {
+ setCountDirty();
+ mTestRunResultMap.remove(testRunName);
+ }
}
diff --git a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
index 301c5eb..41776e9 100644
--- a/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
+++ b/src/com/android/tradefed/result/InvocationToJUnitResultForwarder.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.Log;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.util.TimeUtil;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
@@ -70,9 +71,9 @@
/** {@inheritDoc} */
@Override
public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
- // TODO: no run ended method on TestListener - would be good to propagate the elaspedTime
- // info up
- Log.i(LOG_TAG, String.format("run ended %d ms", elapsedTime));
+ // TODO: no run ended method on TestListener - would be good to propagate the elapsedTime
+ // info up
+ Log.i(LOG_TAG, String.format("Run ended in %s", TimeUtil.formatElapsedTime(elapsedTime)));
}
/**
@@ -90,7 +91,7 @@
@Override
public void testRunStarted(String runName, int testCount) {
// TODO: no run started method on TestResult - would be good to propagate this up
- Log.i(LOG_TAG, String.format("run %s started: %d tests", runName, testCount));
+ Log.i(LOG_TAG, String.format("Running %s: %d tests", runName, testCount));
}
/**
@@ -98,7 +99,9 @@
*/
@Override
public void testRunStopped(long elapsedTime) {
- Log.i(LOG_TAG, String.format("run stopped: %d ms", elapsedTime));
+ Log.i(
+ LOG_TAG,
+ String.format("run stopped after %s", TimeUtil.formatElapsedTime(elapsedTime)));
}
/** {@inheritDoc} */
@@ -159,8 +162,7 @@
*/
@Override
public String toString() {
- // TODO: use ':' or '#' as separator? The eternal debate rages on!
- return String.format("%s:%s", mTestId.getClassName(), mTestId.getTestName());
+ return mTestId.toString();
}
}
diff --git a/src/com/android/tradefed/result/LogSaverResultForwarder.java b/src/com/android/tradefed/result/LogSaverResultForwarder.java
index 6e379a2..fc937e7 100644
--- a/src/com/android/tradefed/result/LogSaverResultForwarder.java
+++ b/src/com/android/tradefed/result/LogSaverResultForwarder.java
@@ -17,9 +17,12 @@
package com.android.tradefed.result;
import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.TestInvocation;
+import com.android.tradefed.log.LogRegistry;
import com.android.tradefed.log.LogUtil.CLog;
import java.io.IOException;
+import java.io.InputStream;
import java.util.List;
/** A {@link ResultForwarder} for saving logs with the global file saver. */
@@ -54,6 +57,7 @@
@Override
public void invocationEnded(long elapsedTime) {
InvocationSummaryHelper.reportInvocationEnded(getListeners(), elapsedTime);
+ reportEndHostLog(mLogSaver);
// Intentionally call invocationEnded for the log saver last.
try {
mLogSaver.invocationEnded(elapsedTime);
@@ -63,6 +67,16 @@
}
}
+ private void reportEndHostLog(ILogSaver saver) {
+ LogRegistry registry = (LogRegistry) LogRegistry.getLogRegistry();
+ try (InputStreamSource source = registry.getLogger().getLog();
+ InputStream stream = source.createInputStream()) {
+ saver.saveLogData(TestInvocation.TRADEFED_END_HOST_LOG, LogDataType.TEXT, stream);
+ } catch (IOException e) {
+ CLog.e(e);
+ }
+ }
+
/**
* {@inheritDoc}
* <p/>
diff --git a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
index 18e8dde..4a0b738 100644
--- a/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
+++ b/src/com/android/tradefed/result/LogcatCrashResultForwarder.java
@@ -38,6 +38,7 @@
/** Special error message from the instrumentation when something goes wrong on device side. */
public static final String ERROR_MESSAGE = "Process crashed.";
+ public static final String SYSTEM_CRASH_MESSAGE = "System has crashed.";
public static final int MAX_NUMBER_CRASH = 3;
@@ -94,7 +95,8 @@
/** Attempt to extract the crash from the logcat if the test was seen as started. */
private String extractCrashAndAddToMessage(String errorMessage, Long startTime) {
- if (errorMessage.contains(ERROR_MESSAGE) && startTime != null) {
+ if ((errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE))
+ && startTime != null) {
mLogcatItem = extractLogcat(mDevice, startTime);
errorMessage = addJavaCrashToString(mLogcatItem, errorMessage);
}
diff --git a/src/com/android/tradefed/result/MergeStrategy.java b/src/com/android/tradefed/result/MergeStrategy.java
deleted file mode 100644
index e64c7fb..0000000
--- a/src/com/android/tradefed/result/MergeStrategy.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.result;
-
-/** Describes how the results should be aggregated when multiple attempts are present. */
-public enum MergeStrategy {
- /** Merging should not be applied and will throw an exception. */
- NO_MERGE,
- /** If a single test case pass then we will consider the merged result passed. */
- ONE_TESTCASE_PASS_IS_PASS,
- /** If a single test run pass then we will consider the merged run result passed. */
- ONE_TESTRUN_PASS_IS_PASS,
- /** If a single run or test cases is a pass we will consider the merged results passed. */
- ANY_PASS_IS_PASS,
- /** If a single run or test cases is failed, status will be failed no matter what. */
- ANY_FAIL_IS_FAIL,
-}
diff --git a/src/com/android/tradefed/result/ResultForwarder.java b/src/com/android/tradefed/result/ResultForwarder.java
index 5034663..fa64e72 100644
--- a/src/com/android/tradefed/result/ResultForwarder.java
+++ b/src/com/android/tradefed/result/ResultForwarder.java
@@ -178,6 +178,19 @@
}
}
+ /** {@inheritDoc} */
+ @Override
+ public void testRunStarted(String runName, int testCount, int attemptNumber, long startTime) {
+ for (ITestInvocationListener listener : mListeners) {
+ try {
+ listener.testRunStarted(runName, testCount, attemptNumber, startTime);
+ } catch (RuntimeException e) {
+ CLog.e("Exception while invoking %s#testRunStarted", listener.getClass().getName());
+ CLog.e(e);
+ }
+ }
+ }
+
/**
* {@inheritDoc}
*/
diff --git a/src/com/android/tradefed/result/SubprocessResultsReporter.java b/src/com/android/tradefed/result/SubprocessResultsReporter.java
index 713fea7..9f9075d 100644
--- a/src/com/android/tradefed/result/SubprocessResultsReporter.java
+++ b/src/com/android/tradefed/result/SubprocessResultsReporter.java
@@ -67,9 +67,11 @@
private IBuildInfo mPrimaryBuildInfo = null;
private Socket mReportSocket = null;
+ private Object mLock = new Object();
private PrintWriter mPrintWriter = null;
private boolean mPrintWarning = true;
+ private boolean mCancelled = false;
/** {@inheritDoc} */
@Override
@@ -138,8 +140,14 @@
/** {@inheritDoc} */
@Override
public void testRunStarted(String runName, int testCount, int attemptNumber) {
+ testRunStarted(runName, testCount, attemptNumber, System.currentTimeMillis());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void testRunStarted(String runName, int testCount, int attemptNumber, long startTime) {
TestRunStartedEventInfo info =
- new TestRunStartedEventInfo(runName, testCount, attemptNumber);
+ new TestRunStartedEventInfo(runName, testCount, attemptNumber, startTime);
printEvent(SubprocessTestResultsParser.StatusKeys.TEST_RUN_STARTED, info);
}
@@ -205,8 +213,7 @@
/** {@inheritDoc} */
@Override
public void logAssociation(String dataName, LogFile logFile) {
- LogAssociationEventInfo info =
- new LogAssociationEventInfo("subprocess-a-" + dataName, logFile);
+ LogAssociationEventInfo info = new LogAssociationEventInfo(dataName, logFile);
printEvent(SubprocessTestResultsParser.StatusKeys.LOG_ASSOCIATION, info);
}
@@ -221,6 +228,9 @@
InvocationEndedEventInfo eventEnd =
new InvocationEndedEventInfo(mPrimaryBuildInfo.getBuildAttributes());
printEvent(SubprocessTestResultsParser.StatusKeys.INVOCATION_ENDED, eventEnd);
+ // Upon invocation ended, trigger the end of the socket when the process finishes
+ SocketFinisher thread = new SocketFinisher();
+ Runtime.getRuntime().addShutdownHook(thread);
}
/**
@@ -276,16 +286,21 @@
}
if(mReportPort != null) {
try {
- if (mReportSocket == null) {
- mReportSocket = new Socket("localhost", mReportPort.intValue());
- mPrintWriter = new PrintWriter(mReportSocket.getOutputStream(), true);
+ if (mCancelled) {
+ return;
}
- if (!mReportSocket.isConnected()) {
- throw new RuntimeException("Reporter Socket is not connected");
+ synchronized (mLock) {
+ if (mReportSocket == null) {
+ mReportSocket = new Socket("localhost", mReportPort.intValue());
+ mPrintWriter = new PrintWriter(mReportSocket.getOutputStream(), true);
+ }
+ if (!mReportSocket.isConnected()) {
+ throw new RuntimeException("Reporter Socket is not connected");
+ }
+ String eventLog = String.format("%s %s\n", key, event.toString());
+ mPrintWriter.print(eventLog);
+ mPrintWriter.flush();
}
- String eventLog = String.format("%s %s\n", key, event.toString());
- mPrintWriter.print(eventLog);
- mPrintWriter.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -302,12 +317,34 @@
/** {@inheritDoc} */
@Override
public void close() {
- StreamUtil.close(mReportSocket);
- StreamUtil.close(mPrintWriter);
+ mCancelled = true;
+ synchronized (mLock) {
+ if (mPrintWriter != null) {
+ mPrintWriter.flush();
+ }
+ StreamUtil.close(mPrintWriter);
+ mPrintWriter = null;
+ StreamUtil.close(mReportSocket);
+ mReportSocket = null;
+ }
}
/** Sets whether or not we should output the test logged or not. */
public void setOutputTestLog(boolean outputTestLog) {
mOutputTestlog = outputTestLog;
}
+
+ /** Threads that help terminating the socket. */
+ private class SocketFinisher extends Thread {
+
+ public SocketFinisher() {
+ super();
+ setName("SubprocessResultsReporter-socket-finisher");
+ }
+
+ @Override
+ public void run() {
+ close();
+ }
+ }
}
diff --git a/src/com/android/tradefed/result/TestResult.java b/src/com/android/tradefed/result/TestResult.java
index 426db04..3d33df3 100644
--- a/src/com/android/tradefed/result/TestResult.java
+++ b/src/com/android/tradefed/result/TestResult.java
@@ -17,6 +17,7 @@
import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.testtype.retry.MergeStrategy;
import com.google.common.base.Joiner;
diff --git a/src/com/android/tradefed/result/TestRunResult.java b/src/com/android/tradefed/result/TestRunResult.java
index ebfcfb1..6be64fc 100644
--- a/src/com/android/tradefed/result/TestRunResult.java
+++ b/src/com/android/tradefed/result/TestRunResult.java
@@ -18,6 +18,7 @@
import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.testtype.retry.MergeStrategy;
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import com.google.common.base.Joiner;
@@ -51,7 +52,8 @@
// Log files associated with the test run itself (testRunStart / testRunEnd).
private Map<String, LogFile> mRunLoggedFiles;
private boolean mIsRunComplete = false;
- private long mElapsedTime = 0;
+ private long mElapsedTime = 0L;
+ private long mStartTime = 0L;
private TestResult mCurrentTestResult;
@@ -163,6 +165,17 @@
return mStatusCounts[status.ordinal()];
}
+ /** Returns all the {@link TestResult} in a particular state. */
+ public List<TestResult> getTestsResultsInState(TestStatus status) {
+ List<TestResult> results = new ArrayList<>();
+ for (TestResult r : mTestResults.values()) {
+ if (r.getStatus().equals(status)) {
+ results.add(r);
+ }
+ }
+ return results;
+ }
+
/** Gets the number of tests in this run. */
public int getNumTests() {
return mTestResults.size();
@@ -188,6 +201,11 @@
return mElapsedTime;
}
+ /** Returns the start time of the first testRunStart call. */
+ public long getStartTime() {
+ return mStartTime;
+ }
+
/** Return the run failure error message, <code>null</code> if run did not fail. */
public String getRunFailureMessage() {
return mRunFailureError;
@@ -210,6 +228,16 @@
* @param testCount the number of expected test cases associated with the test run.
*/
public void testRunStarted(String runName, int testCount) {
+ testRunStarted(runName, testCount, System.currentTimeMillis());
+ }
+
+ /**
+ * Notify that a test run started.
+ *
+ * @param runName the name associated to the test run for tracking purpose.
+ * @param testCount the number of expected test cases associated with the test run.
+ */
+ public void testRunStarted(String runName, int testCount, long startTime) {
// A run may be started multiple times due to crashes or other reasons. Normally the first
// run reflect the expected number of test "testCount". To avoid latter TestRunStarted
// overrides the expected count, only the first testCount will be recorded.
@@ -225,6 +253,9 @@
}
mTestRunName = runName;
mIsRunComplete = false;
+ if (mStartTime == 0L) {
+ mStartTime = startTime;
+ }
// Do not reset mRunFailureError since for re-run we want to preserve previous failures.
}
diff --git a/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParser.java b/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParser.java
new file mode 100644
index 0000000..ad2f208
--- /dev/null
+++ b/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParser.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.result.ddmlib;
+
+import com.android.commands.am.InstrumentationData.ResultsBundle;
+import com.android.commands.am.InstrumentationData.ResultsBundleEntry;
+import com.android.commands.am.InstrumentationData.Session;
+import com.android.commands.am.InstrumentationData.SessionStatus;
+import com.android.commands.am.InstrumentationData.TestStatus;
+import com.android.ddmlib.IShellOutputReceiver;
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.InstrumentationResultParser;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Parses the instrumentation result proto collected during instrumentation test run
+ * and informs ITestRunListener of the results.
+ */
+public class InstrumentationResultProtoParser implements IShellOutputReceiver {
+
+ /** Error message supplied when no test result file is found. */
+ public static final String NO_TEST_RESULTS_FILE = "No instrumentation proto test"
+ + " results file found";
+
+ /** Error message supplied when no test results are received from test run. */
+ public static final String NO_TEST_RESULTS_MSG = "No test results";
+
+ /** Error message supplied when no test result file is found. */
+ public static final String INVALID_TEST_RESULTS_FILE = "Invalid instrumentation proto"
+ + " test results file";
+
+ private static final String INSTRUMENTATION_STATUS_FORMAT = "INSTRUMENTATION_STATUS: %s=%s";
+ private static final String INSTRUMENTATION_STATUS_CODE_FORMAT =
+ "INSTRUMENTATION_STATUS_CODE: %d";
+ private static final String INSTRUMENTATION_RESULT_FORMAT = "INSTRUMENTATION_RESULT: %s=%s";
+ private static final String INSTRUMENTATION_CODE_FORMAT = "INSTRUMENTATION_CODE: %d";
+
+ private InstrumentationResultParser parser;
+
+ public InstrumentationResultProtoParser(String runName,
+ Collection<ITestRunListener> listeners) {
+ parser = new InstrumentationResultParser(runName, listeners);
+ }
+
+ /**
+ * Process the instrumentation result proto file collected during the instrumentation test run.
+ * Instrumentation proto file consist of test status and instrumentation session status. This
+ * method will be used only when complete instrumentation results proto file is available for
+ * parsing.
+ *
+ * @param protoFile that contains the test status and instrumentation session results.
+ * @throws IOException
+ */
+ public void processProtoFile(File protoFile) throws IOException {
+
+ // Report tes run failures in case of null and empty proto file.
+ if (protoFile == null) {
+ parser.handleTestRunFailed(NO_TEST_RESULTS_FILE);
+ return;
+ }
+ if (protoFile.length() == 0) {
+ parser.handleTestRunFailed(NO_TEST_RESULTS_MSG);
+ return;
+ }
+
+ // Read the input proto file
+ byte[] bytesArray = new byte[(int) protoFile.length()];
+ FileInputStream fis = new FileInputStream(protoFile);
+ fis.read(bytesArray);
+ fis.close();
+
+ try {
+ // Parse the proto file.
+ Session instrumentSession = Session.parseFrom(bytesArray);
+
+ // Process multiple test status.
+ List<TestStatus> multipleTestStatus = instrumentSession.getTestStatusList();
+ for (TestStatus teststatus : multipleTestStatus) {
+ processTestStatus(teststatus);
+ }
+
+ // Process instrumentation session status.
+ SessionStatus sessionStatus = instrumentSession.getSessionStatus();
+ if (sessionStatus.isInitialized()) {
+ processSessionStatus(sessionStatus);
+ }
+ } catch (InvalidProtocolBufferException ex) {
+ parser.handleTestRunFailed(INVALID_TEST_RESULTS_FILE);
+ }
+ parser.done();
+ }
+
+ /**
+ * Preprocess the single TestStatus proto message which includes the test info or test
+ * results and result code in to shell output format for further processing by
+ * InstrumentationResultParser.
+ *
+ * @param testStatus The {@link TestStatus} holding the current test info collected during the
+ * test.
+ */
+ public void processTestStatus(TestStatus testStatus) {
+ // Process the test results.
+ ResultsBundle results = testStatus.getResults();
+ List<String> preProcessedLines = new LinkedList<>();
+ for (ResultsBundleEntry entry : results.getEntriesList()) {
+ String currentKey = entry.getKey();
+ String currentValue = null;
+ if (entry.hasValueString()) {
+ currentValue = entry.getValueString().trim();
+ } else if (entry.hasValueInt()) {
+ currentValue = String.valueOf(entry.getValueInt());
+ }
+ preProcessedLines.add(String.format(INSTRUMENTATION_STATUS_FORMAT, currentKey,
+ currentValue));
+ }
+ preProcessedLines.add(String.format(INSTRUMENTATION_STATUS_CODE_FORMAT,
+ testStatus.getResultCode()));
+ parser.processNewLines(preProcessedLines.toArray(new String[preProcessedLines.size()]));
+ }
+
+ /**
+ * Preprocess the instrumentation session status which includes the instrumentation test
+ * results and the session status code to shell output format for further processing by
+ * InstrumentationResultParser.
+ *
+ * @param SessionStatus The {@link SessionStatus} holding the current instrumentation session
+ * info collected during the test run.
+ */
+ public void processSessionStatus(SessionStatus sessionStatus) {
+
+ List<String> preProcessedLines = new LinkedList<>();
+ ResultsBundle results = sessionStatus.getResults();
+ for (ResultsBundleEntry entry : results.getEntriesList()) {
+ String currentKey = entry.getKey();
+ String currentValue = "";
+ if (entry.hasValueString()) {
+ currentValue = entry.getValueString();
+ String lines[] = currentValue.split("\\r?\\n");
+ int lineCount = 1;
+ for (String line : lines) {
+ if (lineCount == 1) {
+ // Only first line should have the Result code prefix.
+ preProcessedLines.add(String.format(INSTRUMENTATION_RESULT_FORMAT,
+ currentKey,
+ line));
+ lineCount++;
+ continue;
+ }
+ preProcessedLines.add(line);
+ }
+ } else if (entry.hasValueInt()) {
+ currentValue = String.valueOf(entry.getValueInt());
+ preProcessedLines.add(String.format(INSTRUMENTATION_RESULT_FORMAT, currentKey,
+ currentValue));
+ }
+ }
+ if (results.isInitialized()) {
+ preProcessedLines.add(String.format(INSTRUMENTATION_CODE_FORMAT,
+ sessionStatus.getResultCode()));
+ }
+
+ parser.processNewLines(preProcessedLines.toArray(new String[preProcessedLines.size()]));
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.IShellOutputReceiver#addOutput(byte[], int, int)
+ */
+ @Override
+ public void addOutput(byte[] protoData, int bytes, int length) {
+ // TODO : Process the streaming proto instrumentation results.
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.IShellOutputReceiver#flush()
+ */
+ @Override
+ public void flush() {
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.IShellOutputReceiver#isCancelled()
+ */
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ /** Set to True to enforce searching for a final time stamp, and fail the run if missing. */
+ public void setEnforceTimeStamp(boolean isEnforced) {
+ parser.setEnforceTimeStamp(isEnforced);
+ }
+}
diff --git a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
index 3bb8f03..0d2fe41 100644
--- a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
@@ -29,10 +29,9 @@
@Option(
name = "proto-output-file",
- description = "File where the proto output will be saved",
- mandatory = true
+ description = "File where the proto output will be saved. If unset, reporter will be inop."
)
- private File mOutputFile;
+ private File mOutputFile = null;
@Option(
name = "periodic-proto-writing",
@@ -44,7 +43,6 @@
// Current index of the sequence of proto output
private int mIndex = 0;
- private File mCurrentOutputFile;
@Override
public void processStartInvocation(
@@ -74,9 +72,6 @@
/** Sets the file where to output the result. */
public void setFileOutput(File output) {
mOutputFile = output;
- if (mPeriodicWriting) {
- mCurrentOutputFile = new File(mOutputFile.getAbsolutePath() + mIndex);
- }
}
/** Enable writing each module individualy to a file. */
@@ -85,12 +80,15 @@
}
private void writeProto(TestRecord record) {
+ if (mOutputFile == null) {
+ return;
+ }
File outputFile = mOutputFile;
if (mPeriodicWriting) {
- outputFile = mCurrentOutputFile;
+ outputFile = new File(mOutputFile.getAbsolutePath() + mIndex);
}
- try {
- record.writeDelimitedTo(new FileOutputStream(outputFile));
+ try (FileOutputStream output = new FileOutputStream(outputFile)) {
+ record.writeDelimitedTo(output);
if (mPeriodicWriting) {
nextOutputFile();
}
@@ -102,6 +100,5 @@
private void nextOutputFile() {
mIndex++;
- mCurrentOutputFile = new File(mOutputFile.getAbsolutePath() + mIndex);
}
}
diff --git a/src/com/android/tradefed/result/proto/ProtoResultParser.java b/src/com/android/tradefed/result/proto/ProtoResultParser.java
index 50e4340..c715a99 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultParser.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultParser.java
@@ -236,7 +236,9 @@
// Get final context in case it changed.
Any anyDescription = endInvocationProto.getDescription();
if (!anyDescription.is(Context.class)) {
- throw new RuntimeException("Expected Any description of type Context");
+ throw new RuntimeException(
+ String.format(
+ "Expected Any description of type Context, was %s", anyDescription));
}
try {
IInvocationContext context =
diff --git a/src/com/android/tradefed/result/proto/ProtoResultReporter.java b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
index 227f518..f415b4c 100644
--- a/src/com/android/tradefed/result/proto/ProtoResultReporter.java
+++ b/src/com/android/tradefed/result/proto/ProtoResultReporter.java
@@ -170,7 +170,9 @@
if (mInvocationFailure != null) {
DebugInfo.Builder debugBuilder = DebugInfo.newBuilder();
- debugBuilder.setErrorMessage(mInvocationFailure.getMessage());
+ if (mInvocationFailure.getMessage() != null) {
+ debugBuilder.setErrorMessage(mInvocationFailure.getMessage());
+ }
debugBuilder.setTrace(StreamUtil.getStackTrace(mInvocationFailure));
mInvocationRecordBuilder.setDebugInfo(debugBuilder);
}
diff --git a/src/com/android/tradefed/result/retry/ISupportGranularResults.java b/src/com/android/tradefed/result/retry/ISupportGranularResults.java
new file mode 100644
index 0000000..7afe5eb
--- /dev/null
+++ b/src/com/android/tradefed/result/retry/ISupportGranularResults.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.retry;
+
+import com.android.tradefed.result.ITestInvocationListener;
+
+/**
+ * Interface specifying whether a {@link ITestInvocationListener} supports receiving the granular
+ * results or not.
+ */
+public interface ISupportGranularResults {
+
+ /** Returns True if the reporter support granular results, false otherwise. */
+ public boolean supportGranularResults();
+}
diff --git a/src/com/android/tradefed/sandbox/SandboxConfigDump.java b/src/com/android/tradefed/sandbox/SandboxConfigDump.java
index a0d526c..0cff43f 100644
--- a/src/com/android/tradefed/sandbox/SandboxConfigDump.java
+++ b/src/com/android/tradefed/sandbox/SandboxConfigDump.java
@@ -121,7 +121,11 @@
pw = new PrintWriter(resFile);
if (DumpCmd.NON_VERSIONED_CONFIG.equals(cmd)) {
// Remove elements that are versioned.
- config.dumpXml(pw, new ArrayList<>(VERSIONED_ELEMENTS));
+ config.dumpXml(
+ pw,
+ new ArrayList<>(VERSIONED_ELEMENTS),
+ true, /* Don't print unchanged options */
+ false);
} else {
// FULL_XML in that case.
config.dumpXml(pw);
diff --git a/src/com/android/tradefed/sandbox/SandboxConfigUtil.java b/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
index 34d3e30..2f67e82 100644
--- a/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
+++ b/src/com/android/tradefed/sandbox/SandboxConfigUtil.java
@@ -86,13 +86,14 @@
mCmdArgs.add(arg);
}
CommandResult result = runUtil.runTimedCmd(DUMP_TIMEOUT, mCmdArgs.toArray(new String[0]));
- CLog.d("stdout: %s", result.getStdout());
- if (result.getStderr() != null && !result.getStderr().isEmpty()) {
- CLog.d("stderr: %s", result.getStderr());
- }
if (CommandStatus.SUCCESS.equals(result.getStatus())) {
return destination;
}
+
+ if (result.getStderr() != null && !result.getStderr().isEmpty()) {
+ CLog.d("stderr: %s", result.getStderr());
+ }
+
FileUtil.deleteFile(destination);
// Do not delete the global configuration file here in this case, it might still be used.
String errorMessage = "Error when dumping the config.";
diff --git a/src/com/android/tradefed/sandbox/TradefedSandbox.java b/src/com/android/tradefed/sandbox/TradefedSandbox.java
index e1f6724..eafa82c 100644
--- a/src/com/android/tradefed/sandbox/TradefedSandbox.java
+++ b/src/com/android/tradefed/sandbox/TradefedSandbox.java
@@ -211,6 +211,7 @@
return e;
}
+ PrettyPrintDelimiter.printStageDelimiter("Sandbox Configuration Preparation");
// Prepare the configuration
Exception res = prepareConfiguration(context, config, listener);
if (res != null) {
@@ -327,8 +328,14 @@
.contains(
String.format(
"Could not find configuration '%s'", args[0]))) {
+ CLog.w(
+ "Child version doesn't contains '%s'. Attempting to backfill missing parent configuration.",
+ args[0]);
File parentConfig = handleChildMissingConfig(args);
if (parentConfig != null) {
+ try (InputStreamSource source = new FileInputStreamSource(parentConfig)) {
+ listener.testLog("sandbox-parent-config", LogDataType.XML, source);
+ }
try {
mSerializedConfiguration =
SandboxConfigUtil.dumpConfigForVersion(
@@ -429,8 +436,9 @@
File tmpParentConfig =
FileUtil.createTempFile("parent-config", ".xml", mSandboxTmpFolder);
PrintWriter pw = new PrintWriter(tmpParentConfig);
- // Do not print deprecated options to avoid compatibility issues
- parentConfig.dumpXml(pw, new ArrayList<>(), false);
+ // Do not print deprecated options to avoid compatibility issues, and do not print
+ // unchanged options.
+ parentConfig.dumpXml(pw, new ArrayList<>(), false, false);
return tmpParentConfig;
} catch (ConfigurationException | IOException e) {
CLog.e("Parent doesn't understand the command either:");
diff --git a/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java b/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
index 92950ee..87366e9 100644
--- a/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
+++ b/src/com/android/tradefed/suite/checker/SystemServerStatusChecker.java
@@ -20,6 +20,9 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
+import com.android.tradefed.util.ProcessInfo;
+
+import java.util.Map;
/**
* Check if the pid of system_server has changed from before and after a module run. A new pid would
@@ -27,18 +30,17 @@
*/
public class SystemServerStatusChecker implements ISystemStatusChecker {
- private String mSystemServerPid = null;
+ private ProcessInfo mSystemServerProcess;
private Long mModuleStartTime = null;
/** {@inheritDoc} */
@Override
public StatusCheckerResult preExecutionCheck(ITestDevice device)
throws DeviceNotAvailableException {
- mSystemServerPid = null;
- mSystemServerPid = device.executeShellCommand("pidof system_server");
+ mSystemServerProcess = device.getProcessByName("system_server");
StatusCheckerResult result = new StatusCheckerResult(CheckStatus.SUCCESS);
- if (mSystemServerPid == null) {
- String message = "Failed to get system_server pid.";
+ if (mSystemServerProcess == null) {
+ String message = "No valid system_server process is found.";
CLog.w(message);
result.setStatus(CheckStatus.FAILED);
result.setBugreportNeeded(true);
@@ -46,13 +48,6 @@
mModuleStartTime = null;
return result;
}
- mSystemServerPid = mSystemServerPid.trim();
- if (!checkValidPid(mSystemServerPid)) {
- CLog.w(
- "Invalid pid response found: '%s'. Skipping the system checker.",
- mSystemServerPid);
- mSystemServerPid = null;
- }
mModuleStartTime = getCurrentTime();
return result;
}
@@ -61,34 +56,53 @@
@Override
public StatusCheckerResult postExecutionCheck(ITestDevice device)
throws DeviceNotAvailableException {
- if (mSystemServerPid == null) {
- CLog.d("No valid known value of system_server pid, skipping system checker.");
+ if (mSystemServerProcess == null) {
+ CLog.d(
+ "No valid system_server process was found in preExecutionCheck, "
+ + "skipping system_server postExecutionCheck.");
return new StatusCheckerResult(CheckStatus.SUCCESS);
}
- String tmpSystemServerPid = device.executeShellCommand("pidof system_server");
- if (tmpSystemServerPid != null) {
- tmpSystemServerPid = tmpSystemServerPid.trim();
+ String message = null;
+ ProcessInfo currSystemServerProcess = device.getProcessByName("system_server");
+ if (currSystemServerProcess == null) {
+ message = "system_server is down";
+ CLog.w(message);
+ StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
+ result.setBugreportNeeded(true);
+ result.setErrorMessage(message);
+ return result;
}
- if (mSystemServerPid.equals(tmpSystemServerPid)) {
+
+ if (currSystemServerProcess.getPid() == mSystemServerProcess.getPid()
+ && currSystemServerProcess.getStartTime() == mSystemServerProcess.getStartTime()) {
return new StatusCheckerResult(CheckStatus.SUCCESS);
}
- String message =
- String.format(
- "system_server has a different pid after the module run. from %s to %s",
- mSystemServerPid, tmpSystemServerPid);
+ //system_server restarted
+ Map<Long, String> bootHistory =
+ device.getBootHistorySince(mSystemServerProcess.getStartTime());
+ CLog.i("The device reboot with boot history: %s", bootHistory);
+ if (bootHistory.isEmpty()) {
+ message = "system_server restarted without device reboot";
+ } else {
+ message = "system_server restarted with device boot history: " + bootHistory.toString();
+ // Check if there is a TF triggered reboot with device.doReboot
+ long lastExpectedReboot = device.getLastExpectedRebootTimeMillis();
+ if (mModuleStartTime != null && lastExpectedReboot < mModuleStartTime) {
+ // The reboot is not triggered by Tradefed host.
+ CLog.w(
+ "System_server restarted and Tradefed didn't trigger a reboot: "
+ + "last expected reboot: %s, module start time: %s, "
+ + "something went wrong.",
+ lastExpectedReboot, mModuleStartTime);
+ } else {
+ // The reboot is triggered by Tradefed host
+ CLog.i("Tradefed triggered reboot detected");
+ return new StatusCheckerResult(CheckStatus.SUCCESS);
+ }
+ }
CLog.w(message);
StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
- // TODO: evaluate if we still need to fail the checker if it was a TF reboot
- long lastExpectedReboot = device.getLastExpectedRebootTimeMillis();
- if (mModuleStartTime != null && lastExpectedReboot < mModuleStartTime) {
- // In case no Tradefed reboot was triggered, we capture a bugreport to figure this out.
- CLog.w(
- "System_server pid changed and Tradefed didn't trigger a reboot: "
- + "last expected reboot: %s, module start time: %s, "
- + "something went wrong.",
- lastExpectedReboot, mModuleStartTime);
- result.setBugreportNeeded(true);
- }
+ result.setBugreportNeeded(true);
result.setErrorMessage(message);
return result;
}
@@ -99,17 +113,4 @@
return System.currentTimeMillis();
}
- /** Validate that pid is an integer and not empty. */
- private boolean checkValidPid(String output) {
- if (output.isEmpty()) {
- return false;
- }
- try {
- Integer.parseInt(output);
- } catch (NumberFormatException e) {
- return false;
- }
-
- return true;
- }
}
diff --git a/src/com/android/tradefed/suite/checker/UserChecker.java b/src/com/android/tradefed/suite/checker/UserChecker.java
index aa84de1..e900191 100644
--- a/src/com/android/tradefed/suite/checker/UserChecker.java
+++ b/src/com/android/tradefed/suite/checker/UserChecker.java
@@ -15,18 +15,17 @@
*/
package com.android.tradefed.suite.checker;
-import java.util.List;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Collection;
+import java.util.Map;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
-import com.android.tradefed.util.UserUtil;
-import com.android.tradefed.util.UserUtil.UserSwitchFailedException;
/**
* Checks if users have changed during the test.
@@ -40,33 +39,58 @@
name = "user-type",
description = "The type of user to switch to before each module run."
)
- private UserUtil.UserType mUserToSwitchTo = UserUtil.UserType.CURRENT;
+ private UserInfo.UserType mUserToSwitchTo = UserInfo.UserType.CURRENT;
- public static final String DEFAULT_NAME = "TFauto";
+ @Option(
+ name = "user-cleanup",
+ description =
+ "If true, attempt to cleanup any changes made to users:"
+ + "\n - switch to previous current-user"
+ + "\n - remove any created users"
+ + "\n\nThis does NOT:"
+ + "\n - attempt to re-create a user that was deleted"
+ + "\n - start/stop existing users if their running status changed"
+ )
+ private boolean mCleanup = false;
- private DeviceUserState mPreExecutionUserState;
+ private UserInfo mPreCurrentUserInfo = null;
+ private Map<Integer, UserInfo> mPreUsersInfo = null;
+ private int mSwitchedToUserId = -1;
/** {@inheritDoc} */
@Override
public StatusCheckerResult preExecutionCheck(ITestDevice device)
throws DeviceNotAvailableException {
- String userSwitchErrorMsg = null;
- try {
- switchToExistingOrCreateUserType(device);
- } catch (UserSwitchFailedException err) {
- userSwitchErrorMsg = err.toString();
+ mPreUsersInfo = device.getUserInfos();
+ mPreCurrentUserInfo = mPreUsersInfo.get(device.getCurrentUser());
+
+ if (mPreCurrentUserInfo.isUserType(mUserToSwitchTo, mPreCurrentUserInfo.userId())) {
+ CLog.i(
+ "Current user %d is already user type %s, no action.",
+ mPreCurrentUserInfo.userId(), mUserToSwitchTo.toString());
+ return new StatusCheckerResult(CheckStatus.SUCCESS);
}
- mPreExecutionUserState = new DeviceUserState(device);
- CLog.d("preExecutionUsers=" + mPreExecutionUserState);
+ mSwitchedToUserId = findMatchingUser(mPreUsersInfo.values());
+ if (mSwitchedToUserId < 0) {
+ mSwitchedToUserId =
+ device.createUser(
+ /* name= */ "Tf" + mUserToSwitchTo.toString().toLowerCase(),
+ /* guest= */ mUserToSwitchTo.isGuest(),
+ /* ephemeral= */ false);
+ CLog.i(
+ "No user of type %s found, created user %d",
+ mUserToSwitchTo.toString(), mSwitchedToUserId);
+ }
- if (userSwitchErrorMsg == null) {
- return new StatusCheckerResult(CheckStatus.SUCCESS);
+ CLog.i(
+ "Current user is %d, switching to user %s of type %s",
+ mPreCurrentUserInfo.userId(), mSwitchedToUserId, mUserToSwitchTo);
+ if (!device.switchUser(mSwitchedToUserId)) {
+ return statusFail(String.format("Failed to switch to user %d", mSwitchedToUserId));
} else {
- StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
- result.setErrorMessage(userSwitchErrorMsg);
- return result;
+ return new StatusCheckerResult(CheckStatus.SUCCESS);
}
}
@@ -74,145 +98,79 @@
@Override
public StatusCheckerResult postExecutionCheck(ITestDevice device)
throws DeviceNotAvailableException {
- DeviceUserState postDeviceUserState = new DeviceUserState(device);
- CLog.d("postExecutionUsers=" + postDeviceUserState);
+ Map<Integer, UserInfo> postUsersInfo = device.getUserInfos();
+ UserInfo postCurrentUserInfo = postUsersInfo.get(device.getCurrentUser());
ArrayList<String> errors = new ArrayList<>();
- for (Integer removedUser : mPreExecutionUserState.findRemovedUsers(postDeviceUserState)) {
- errors.add(String.format("User %d no longer exists after test", removedUser));
+ if (mPreCurrentUserInfo.userId() != postCurrentUserInfo.userId()) {
+ if (postCurrentUserInfo.userId() != mSwitchedToUserId) {
+ errors.add(
+ String.format(
+ "User %d was the currentUser before, has changed to %d",
+ mPreCurrentUserInfo.userId(), postCurrentUserInfo.userId()));
+ }
+ if (mCleanup) {
+ if (!device.switchUser(mPreCurrentUserInfo.userId())) {
+ errors.add(
+ String.format(
+ "Failed to switch back to previous current user %d."
+ + " Check if it was removed.",
+ mPreCurrentUserInfo.userId()));
+ }
+ }
}
- for (Integer addedUser : mPreExecutionUserState.findAddedUsers(postDeviceUserState)) {
- errors.add(
- String.format(
- "User %d was created during the test and not deleted", addedUser));
+ for (UserInfo preUserInfo : mPreUsersInfo.values()) {
+ int preUserId = preUserInfo.userId();
+ if (!postUsersInfo.containsKey(preUserId)) {
+ errors.add(String.format("User %d no longer exists after test", preUserId));
+ continue;
+ }
+
+ UserInfo postUserInfo = postUsersInfo.get(preUserId);
+ if (preUserInfo.isRunning() != postUserInfo.isRunning()) {
+ CLog.w(
+ "User %d running status changed from %b -> %b",
+ preUserId, preUserInfo.isRunning(), postUserInfo.isRunning());
+ }
}
- if (mPreExecutionUserState.currentUserChanged(postDeviceUserState)) {
- errors.add(
- String.format(
- "User %d was the currentUser before, has changed to %d",
- mPreExecutionUserState.getCurrentUser(),
- postDeviceUserState.getCurrentUser()));
- }
-
- for (int userId : mPreExecutionUserState.findStoppedUsers(postDeviceUserState)) {
- CLog.w("User %d was running but is now stopped.", userId);
- }
-
- for (int userId : mPreExecutionUserState.findStartedUsers(postDeviceUserState)) {
- CLog.w("User %d was stopped but is now running.", userId);
+ for (int postUserId : postUsersInfo.keySet()) {
+ if (!mPreUsersInfo.containsKey(postUserId)) {
+ if (mSwitchedToUserId != postUserId) {
+ errors.add(
+ String.format(
+ "User %d was created during test and not deleted", postUserId));
+ }
+ if (mCleanup) {
+ if (!device.removeUser(postUserId)) {
+ errors.add(String.format("Failed to remove new user %d", postUserId));
+ }
+ }
+ }
}
if (errors.size() > 0) {
- StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
- result.setErrorMessage(String.join("\n", errors));
- return result;
+ return statusFail(String.join("\n", errors));
} else {
return new StatusCheckerResult(CheckStatus.SUCCESS);
}
}
- /**
- * Switches to the mUserType, creating if necessary.
- *
- * <p>Returns null if success, the error string if there is an error.
- */
- private void switchToExistingOrCreateUserType(ITestDevice device)
- throws DeviceNotAvailableException, UserSwitchFailedException {
- try {
- UserUtil.switchToUserType(device, mUserToSwitchTo);
- } catch (UserUtil.SecondaryUserNotFoundException attemptCreate) {
- CLog.d("No secondary users exist, creating one.");
- int secondary = device.createUserNoThrow(DEFAULT_NAME);
- if (secondary <= 0) {
- throw new UserSwitchFailedException("Failed to create secondary user");
+ /** Return the userId of a matching user, or -1 if none match. */
+ private int findMatchingUser(Collection<UserInfo> usersInfo) {
+ for (UserInfo userInfo : mPreUsersInfo.values()) {
+ if (userInfo.isUserType(mUserToSwitchTo, mPreCurrentUserInfo.userId())) {
+ return userInfo.userId();
}
- UserUtil.switchToUserType(device, mUserToSwitchTo);
}
+ return -1;
}
- /** Class for monitoring changes to the user state between pre/post check. */
- static class DeviceUserState {
- private final int mCurrentUser;
- private final ArrayList<Integer> mUsers;
- private final HashMap<Integer, Boolean> mUserRunningStates;
-
- DeviceUserState(ITestDevice device) throws DeviceNotAvailableException {
- mCurrentUser = device.getCurrentUser();
- mUsers = device.listUsers();
- mUserRunningStates = new HashMap<>(mUsers.size());
- for (Integer userId : mUsers) {
- mUserRunningStates.put(userId, device.isUserRunning(userId));
- }
- }
-
- public int getCurrentUser() {
- return mCurrentUser;
- }
-
- @Override
- public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append(String.format("currentUser=%d;", getCurrentUser()));
- for (Integer userId : mUsers) {
- String running = mUserRunningStates.get(userId) ? "running" : "stopped";
- builder.append(String.format(" %d:%s", userId, running));
- }
- return builder.toString();
- }
-
- List<Integer> findRemovedUsers(DeviceUserState otherState) {
- ArrayList<Integer> removedUsers = new ArrayList<>();
- for (Integer userId : mUsers) {
- if (!otherState.containsUser(userId)) {
- removedUsers.add(userId);
- }
- }
- return removedUsers;
- }
-
- List<Integer> findAddedUsers(DeviceUserState otherState) {
- ArrayList<Integer> addedUsers = new ArrayList<>();
- for (Integer userId : otherState.mUsers) {
- if (!this.containsUser(userId)) {
- addedUsers.add(userId);
- }
- }
- return addedUsers;
- }
-
- boolean currentUserChanged(DeviceUserState otherState) {
- return this.getCurrentUser() != otherState.getCurrentUser();
- }
-
- List<Integer> findStartedUsers(DeviceUserState otherState) {
- ArrayList<Integer> startedUsers = new ArrayList<>();
- for (Integer userId : mUsers) {
- if (!this.isUserRunning(userId) && otherState.isUserRunning(userId)) {
- startedUsers.add(userId);
- }
- }
- return startedUsers;
- }
-
- List<Integer> findStoppedUsers(DeviceUserState otherState) {
- ArrayList<Integer> stoppedUsers = new ArrayList<>();
- for (Integer userId : mUsers) {
- if (this.isUserRunning(userId) && !otherState.isUserRunning(userId)) {
- stoppedUsers.add(userId);
- }
- }
- return stoppedUsers;
- }
-
- private boolean containsUser(int userId) {
- return mUserRunningStates.containsKey(userId);
- }
-
- private boolean isUserRunning(int userId) {
- return mUserRunningStates.getOrDefault(userId, /* default= */ false);
- }
+ private static StatusCheckerResult statusFail(String msg) {
+ StatusCheckerResult result = new StatusCheckerResult(CheckStatus.FAILED);
+ result.setErrorMessage(msg);
+ return result;
}
}
diff --git a/src/com/android/tradefed/targetprep/AppSetup.java b/src/com/android/tradefed/targetprep/AppSetup.java
index c1268a0..0bc25da 100644
--- a/src/com/android/tradefed/targetprep/AppSetup.java
+++ b/src/com/android/tradefed/targetprep/AppSetup.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.targetprep;
-import com.android.tradefed.build.IAppBuildInfo;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.VersionedFile;
import com.android.tradefed.config.Option;
@@ -86,10 +85,10 @@
@Override
public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
DeviceNotAvailableException, BuildError {
- if (!(buildInfo instanceof IAppBuildInfo)) {
- throw new IllegalArgumentException("Provided buildInfo is not a AppBuildInfo");
+ List<VersionedFile> apps = buildInfo.getAppPackageFiles();
+ if (apps.isEmpty()) {
+ return;
}
- IAppBuildInfo appBuild = (IAppBuildInfo)buildInfo;
CLog.i("Performing setup on %s", device.getSerialNumber());
// double check that device is clean, in case it has unexpected cruft on it
@@ -103,7 +102,7 @@
}
if (mInstall) {
- for (VersionedFile apkFile : appBuild.getAppPackageFiles()) {
+ for (VersionedFile apkFile : apps) {
if (mCheckMinSdk) {
AaptParser aaptParser = doAaptParse(apkFile.getFile());
if (aaptParser == null) {
diff --git a/src/com/android/tradefed/targetprep/CreateUserPreparer.java b/src/com/android/tradefed/targetprep/CreateUserPreparer.java
index c379be4..aeb835d 100644
--- a/src/com/android/tradefed/targetprep/CreateUserPreparer.java
+++ b/src/com/android/tradefed/targetprep/CreateUserPreparer.java
@@ -43,11 +43,18 @@
} catch (IllegalStateException e) {
throw new TargetSetupError("Failed to create user.", e, device.getDeviceDescriptor());
}
+ if (!device.startUser(mCreatedUserId, true)) {
+ throw new TargetSetupError(
+ String.format("Failed to start to user '%s'", mCreatedUserId),
+ device.getDeviceDescriptor());
+ }
if (!device.switchUser(mCreatedUserId)) {
throw new TargetSetupError(
String.format("Failed to switch to user '%s'", mCreatedUserId),
device.getDeviceDescriptor());
}
+ device.waitForDeviceAvailable();
+ device.postBootSetup();
}
@Override
diff --git a/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java b/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java
new file mode 100644
index 0000000..e10ee76
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/DeviceBuildInfoBootStrapper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.BuildInfoUtil;
+
+/**
+ * A {@link ITargetPreparer} that replaces build info fields with attributes read from device
+ *
+ * <p>This is useful for testing devices with builds generated from an external source (e.g.
+ * external partner devices)
+ *
+ * @see {@link DeviceBuildInfoInjector}, {@link BootstrapBuildProvider}
+ */
+public class DeviceBuildInfoBootStrapper extends BaseTargetPreparer {
+
+ @Option(name = "override-device-build-id", description = "the device buid id to inject.")
+ private String mOverrideDeviceBuildId = null;
+
+ @Option(name = "override-device-build-alias", description = "the device buid alias to inject.")
+ private String mOverrideDeviceBuildAlias = null;
+
+ @Option(
+ name = "override-device-build-flavor",
+ description = "the device build flavor to inject."
+ )
+ private String mOverrideDeviceBuildFlavor = null;
+
+ @Option(
+ name = "override-device-build-branch",
+ description = "the device build branch to inject."
+ )
+ private String mOverrideDeviceBuildBranch = null;
+
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, BuildError, DeviceNotAvailableException {
+ BuildInfoUtil.bootstrapDeviceBuildAttributes(
+ buildInfo,
+ device,
+ mOverrideDeviceBuildId,
+ mOverrideDeviceBuildFlavor,
+ mOverrideDeviceBuildBranch,
+ mOverrideDeviceBuildAlias);
+ }
+}
diff --git a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
index 41d41e3..24d72d1 100644
--- a/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
+++ b/src/com/android/tradefed/targetprep/DeviceFlashPreparer.java
@@ -32,6 +32,8 @@
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;
+import com.google.common.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
@@ -106,6 +108,12 @@
)
private Collection<String> mFastbootFlashOptions = new ArrayList<>();
+ @Option(
+ name = "flash-ramdisk",
+ description =
+ "flashes ramdisk (boot partition) in addition " + "to regular system image")
+ private boolean mShouldFlashRamdisk = false;
+
/**
* Sets the device boot time
* <p/>
@@ -176,6 +184,11 @@
if (!(buildInfo instanceof IDeviceBuildInfo)) {
throw new IllegalArgumentException("Provided buildInfo is not a IDeviceBuildInfo");
}
+ IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) buildInfo;
+ if (mShouldFlashRamdisk && deviceBuild.getRamdiskFile() == null) {
+ throw new IllegalArgumentException(
+ "ramdisk flashing enabled but no ramdisk file was found in build info");
+ }
// don't allow interruptions during flashing operations.
getRunUtil().allowInterrupt(false);
IDeviceManager deviceManager = getDeviceManager();
@@ -183,7 +196,6 @@
long flashingTime = -1;
long start = -1;
try {
- IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo)buildInfo;
checkDeviceProductType(device, deviceBuild);
device.setRecoveryMode(RecoveryMode.ONLINE);
IDeviceFlasher flasher = createFlasher(device);
@@ -200,6 +212,7 @@
flasher.setUserDataFlashOption(mUserDataFlashOption);
flasher.setForceSystemFlash(mForceSystemFlash);
flasher.setDataWipeSkipList(mDataWipeSkipList);
+ flasher.setShouldFlashRamdisk(mShouldFlashRamdisk);
if (flasher instanceof FastbootDeviceFlasher) {
((FastbootDeviceFlasher) flasher).setFlashOptions(mFastbootFlashOptions);
}
@@ -447,4 +460,14 @@
String serial, long queueTime, long flashingTime, CommandStatus flashingStatus) {
// no-op as default implementation
}
+
+ /**
+ * Sets the option for whether ramdisk should be flashed
+ *
+ * @param shouldFlashRamdisk
+ */
+ @VisibleForTesting
+ void setShouldFlashRamdisk(boolean shouldFlashRamdisk) {
+ mShouldFlashRamdisk = shouldFlashRamdisk;
+ }
}
diff --git a/src/com/android/tradefed/targetprep/DeviceImageZipFlashingTargetPreparer.java b/src/com/android/tradefed/targetprep/DeviceImageZipFlashingTargetPreparer.java
new file mode 100644
index 0000000..d75558d
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/DeviceImageZipFlashingTargetPreparer.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+import com.android.tradefed.util.ZipUtil2;
+
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A target preparer that flashes the device with device images provided via a specific format.
+ *
+ * <p>High level requirements for the device image format:
+ *
+ * <ul>
+ * <li>Device image file must be a zip file
+ * <li>The zip file must include a flash-all.sh script at the root
+ * <li>The script must assume that the device is in userspace visible to <code>adb devices</code>
+ * <li>The rest of the zip file will be extracted into the same location as script with the same
+ * directory layout, and the script may make reference to any files packaged in the zip via
+ * relative path
+ * <li>After flashing, the script must return the device to the same state
+ * <li>An environment variable <code>ANDROID_SERIAL</code> will be set to device serial number as
+ * part of the execution environment
+ * <li>The script may assume that it has <code>adb</code> and <code>fastboot</code> on PATH
+ * </ul>
+ *
+ * This target preparer will unpack the device image zip file and execute the enclosed <code>flash-
+ * all.sh</code> under the assumptions outline in requirements above.
+ */
+public class DeviceImageZipFlashingTargetPreparer extends DeviceUpdateTargetPreparer {
+
+ private static final String ANDROID_SERIAL_ENV = "ANDROID_SERIAL";
+
+ @Option(name = "device-image-zip", description = "the device image zip file to be flashed")
+ private File mDeviceImageZip = null;
+
+ @Option(
+ name = "flashing-timeout",
+ description = "timeout for flashing the device images",
+ isTimeVal = true
+ )
+ // defaults to 10m: assuming USB 2.0 transfer speed, concurrency and some buffer
+ private long mFlashingTimeout = 10 * 60 * 1000;
+
+ @Option(
+ name = "flashing-script",
+ description =
+ "the name of the flashing script bundled within " + "the device image zip file"
+ )
+ private String mFlashingScript = "flash-all.sh";
+
+ /** {@inheritDoc} */
+ @Override
+ protected File getDeviceUpdateImage() {
+ return mDeviceImageZip;
+ }
+
+ /** No-op */
+ @Override
+ protected void preUpdateActions(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {}
+
+ /** No-op */
+ @Override
+ protected void postUpdateActions(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {}
+
+ /** Expands the device image update zip and calls the enclosed flashing script */
+ @Override
+ protected void performDeviceUpdate(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {
+ // first unzip the package
+ File extractedImage = null;
+ try {
+ extractedImage = extractZip(device, getDeviceUpdateImage());
+ File flashingScript = new File(extractedImage, mFlashingScript);
+ if (!flashingScript.exists()) {
+ throw new TargetSetupError(
+ String.format(
+ "Flashing script \"%s\" not found inside " + "the device image zip",
+ mFlashingScript),
+ device.getDeviceDescriptor());
+ }
+ IRunUtil runUtil = new RunUtil();
+ runUtil.setEnvVariable(ANDROID_SERIAL_ENV, device.getSerialNumber());
+ runUtil.setWorkingDir(extractedImage);
+ CLog.i("Starting flashing on %s", device.getSerialNumber());
+ CommandResult result =
+ runUtil.runTimedCmd(
+ mFlashingTimeout, "bash", "-x", flashingScript.getAbsolutePath());
+ CommandStatus status = result.getStatus();
+ StringBuilder sb = new StringBuilder();
+ sb.append(
+ String.format(
+ "Flashing command finished with status: %s\n", status.toString()));
+ sb.append(String.format("Flashing command stdout:\n%s\n", result.getStdout()));
+ sb.append(String.format("Flashing command stderr:\n%s\n", result.getStderr()));
+ if (!CommandStatus.SUCCESS.equals(status)) {
+ CLog.w(sb.toString());
+ } else {
+ CLog.v(sb.toString());
+ }
+ String message =
+ String.format(
+ "Flashing script failed (status: %s), "
+ + "check host logs above for details",
+ status.toString());
+ switch (status) {
+ case SUCCESS:
+ break;
+ case FAILED:
+ throw new TargetSetupError(message, device.getDeviceDescriptor());
+ case EXCEPTION:
+ throw new TargetSetupError(message, device.getDeviceDescriptor());
+ case TIMED_OUT:
+ throw new TargetSetupError(message, device.getDeviceDescriptor());
+ default:
+ throw new IllegalStateException("Failsafe: not expected");
+ }
+ } finally {
+ FileUtil.recursiveDelete(extractedImage);
+ }
+ }
+
+ /**
+ * Extract a zip file and return temporary directory with contents.
+ *
+ * @param device the {@link ITestDevice}
+ * @param zip {@link File} to unzip
+ * @throws TargetSetupError if any operation fails
+ */
+ private static File extractZip(ITestDevice device, File zip) throws TargetSetupError {
+ ZipFile zFile = null;
+ File outputDir;
+ try {
+ zFile = new ZipFile(zip);
+ File fastbootTmpDir =
+ GlobalConfiguration.getInstance().getHostOptions().getFastbootTmpDir();
+ outputDir =
+ FileUtil.createTempDir(
+ DeviceImageZipFlashingTargetPreparer.class.getSimpleName()
+ + "-tmp-files",
+ fastbootTmpDir);
+ ZipUtil2.extractZip(zFile, outputDir);
+ } catch (IOException | IllegalStateException exception) {
+ throw new TargetSetupError(
+ exception.getMessage(), exception, device.getDeviceDescriptor());
+ } finally {
+ ZipUtil2.closeZip(zFile);
+ }
+ return outputDir;
+ }
+}
diff --git a/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java b/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java
new file mode 100644
index 0000000..9c81fb1
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/DeviceUpdateTargetPreparer.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceUnresponsiveException;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.ITestDevice.RecoveryMode;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An abstract {@link ITargetPreparer} that takes care of common steps around updating devices with
+ * a device image file from an external source (as opposed to a build service). The actual update
+ * mechanism is delegated to implementor of subclasses.
+ */
+public abstract class DeviceUpdateTargetPreparer extends DeviceBuildInfoBootStrapper {
+
+ @Option(
+ name = "device-boot-time",
+ description = "max time to wait for device to boot.",
+ isTimeVal = true
+ )
+ private long mDeviceBootTime = 5 * 60 * 1000;
+
+ @Option(
+ name = "bootstrap-build-info",
+ description =
+ "whether build info should be"
+ + "bootstrapped based on device attributes after flashing"
+ )
+ private boolean mBootStrapBuildInfo = true;
+
+ /** {@inheritDoc} */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, BuildError, DeviceNotAvailableException {
+ File deviceUpdateImage = getDeviceUpdateImage();
+ if (deviceUpdateImage == null) {
+ CLog.i("No device image zip file provided, assuming no-op; skipping ...");
+ return;
+ }
+ if (!deviceUpdateImage.exists()) {
+ throw new TargetSetupError(
+ "Device image file not found: " + deviceUpdateImage.getAbsolutePath(),
+ device.getDeviceDescriptor());
+ }
+ preUpdateActions(deviceUpdateImage, device);
+ // flashing concurrency control
+ long start = System.currentTimeMillis();
+ IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance();
+ deviceManager.takeFlashingPermit();
+ CLog.v(
+ "Flashing permit obtained after %ds",
+ TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
+ try {
+ performDeviceUpdate(deviceUpdateImage, device);
+ } finally {
+ CLog.v(
+ "Flashing finished after %ds",
+ TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis() - start)));
+ deviceManager.returnFlashingPermit();
+ }
+ postUpdateActions(deviceUpdateImage, device);
+ CLog.i(
+ "Flashing completed successfully on %s, waiting for device to boot up.",
+ device.getSerialNumber());
+ device.waitForDeviceOnline();
+ // device may lose date setting if wiped, update with host side date in case anything on
+ // device side malfunction with an invalid date
+ if (device.getOptions().isEnableAdbRoot()) {
+ boolean rootEnabled = device.enableAdbRoot();
+ if (rootEnabled) {
+ device.setDate(null);
+ }
+ }
+ try {
+ device.setRecoveryMode(RecoveryMode.AVAILABLE);
+ device.waitForDeviceAvailable(mDeviceBootTime);
+ } catch (DeviceUnresponsiveException e) {
+ // assume this is a build problem
+ throw new DeviceFailedToBootError(
+ String.format(
+ "Device %s did not become available after flashing %s",
+ device.getSerialNumber(), deviceUpdateImage.getAbsolutePath()),
+ device.getDeviceDescriptor());
+ }
+ CLog.i("Device update completed on %s", device.getDeviceDescriptor());
+ // calling this last because we want to inject device side build info after device boots up
+ if (mBootStrapBuildInfo) {
+ super.setUp(device, buildInfo);
+ }
+ }
+
+ /**
+ * Provides a {@link File} instance representing the device image file to be used for updating
+ */
+ protected abstract File getDeviceUpdateImage();
+
+ /**
+ * Actions to be performed before the device is updated. This method will be called outside of
+ * flashing concurrency control.
+ *
+ * @param deviceUpdateImage
+ * @param device
+ * @throws TargetSetupError
+ */
+ protected abstract void preUpdateActions(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError;
+
+ /**
+ * Performs the device image update on device
+ *
+ * @param deviceUpdateImage
+ * @param device
+ * @throws TargetSetupError
+ */
+ protected abstract void performDeviceUpdate(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError;
+
+ /**
+ * Actions to be performed after the device is updated but before post update setup steps are
+ * performed. This method will be called outside of flashing concurrency control.
+ *
+ * @param deviceUpdateImage
+ * @param device
+ * @throws TargetSetupError
+ */
+ protected abstract void postUpdateActions(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError;
+}
diff --git a/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java b/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
index 6d73d68..c1a6f8f 100644
--- a/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
+++ b/src/com/android/tradefed/targetprep/FastbootDeviceFlasher.java
@@ -43,10 +43,8 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-/**
- * A class that relies on fastboot to flash an image on physical Android hardware.
- */
-public class FastbootDeviceFlasher implements IDeviceFlasher {
+/** A class that relies on fastboot to flash an image on physical Android hardware. */
+public class FastbootDeviceFlasher implements IDeviceFlasher {
public static final String BASEBAND_IMAGE_NAME = "radio";
private static final String FASTBOOT_VERSION = "fastboot_version";
@@ -55,6 +53,7 @@
private static final String SLOT_PROP = "ro.boot.slot_suffix";
private static final String SLOT_VAR = "current-slot";
+ private static final String SKIP_REBOOT_PARAM = "--skip-reboot";
private long mWipeTimeout = 4 * 60 * 1000;
@@ -74,6 +73,8 @@
private CommandStatus mSystemFlashStatus;
+ private boolean mShouldFlashRamdisk = false;
+
/**
* {@inheritDoc}
*/
@@ -110,7 +111,7 @@
// Lazily initialize the TestZipInstaller.
if (mTestsZipInstaller == null) {
if (mDataWipeSkipList == null) {
- mDataWipeSkipList = new ArrayList<String> ();
+ mDataWipeSkipList = new ArrayList<String>();
}
if (mDataWipeSkipList.isEmpty()) {
// To maintain backwards compatibility. Keep media by default.
@@ -164,9 +165,14 @@
checkAndFlashSystem(device, systemBuildId, systemBuildFlavor, deviceBuild);
}
- private String[] buildFastbootCommand(String action, String... args) {
+ private String[] buildFastbootCommand(String action, boolean skipReboot, String... args) {
List<String> cmdArgs = new ArrayList<>();
if ("flash".equals(action) || "update".equals(action)) {
+ if (skipReboot) {
+ // need to skip reboot if flashing root ramdisk, because this will be typically
+ // used together with flashing of user build, and
+ cmdArgs.add(SKIP_REBOOT_PARAM);
+ }
cmdArgs.addAll(mFlashOptions);
}
cmdArgs.add(action);
@@ -216,7 +222,9 @@
throws DeviceNotAvailableException, TargetSetupError {
CLog.d("fastboot flash %s %s", partition, imgFile.getAbsolutePath());
executeLongFastbootCmd(
- device, buildFastbootCommand("flash", partition, imgFile.getAbsolutePath()));
+ device,
+ buildFastbootCommand(
+ "flash", mShouldFlashRamdisk, partition, imgFile.getAbsolutePath()));
}
/**
@@ -257,8 +265,7 @@
*
* @param device the {@link ITestDevice} to download resources for
* @param localBuild the {@link IDeviceBuildInfo} to populate. Assumes device image file is
- * already set
- *
+ * already set
* @throws DeviceNotAvailableException if device is not available
* @throws TargetSetupError if failed to retrieve resources
*/
@@ -284,8 +291,10 @@
// only set bootloader image if this build doesn't have one already
// TODO: move this logic to the BuildProvider step
if (bootloaderVersion != null && localBuild.getBootloaderImageFile() == null) {
- localBuild.setBootloaderImageFile(getFlashingResourcesRetriever().retrieveFile(
- getBootloaderFilePrefix(device), bootloaderVersion), bootloaderVersion);
+ localBuild.setBootloaderImageFile(
+ getFlashingResourcesRetriever()
+ .retrieveFile(getBootloaderFilePrefix(device), bootloaderVersion),
+ bootloaderVersion);
}
String basebandVersion = resourceParser.getRequiredBasebandVersion();
// only set baseband image if this build doesn't have one already
@@ -298,18 +307,18 @@
/**
* Verify that the device's product type supports the build-to-be-flashed.
- * <p/>
- * The base implementation will verify that the deviceProductType is included in the
- * {@link IFlashingResourcesParser#getRequiredBoards()} collection. Subclasses may override
- * as desired.
+ *
+ * <p>The base implementation will verify that the deviceProductType is included in the {@link
+ * IFlashingResourcesParser#getRequiredBoards()} collection. Subclasses may override as desired.
*
* @param device the {@link ITestDevice} to be flashed
* @param resourceParser the {@link IFlashingResourcesParser}
* @param deviceProductType the <var>device</var>'s product type
* @throws TargetSetupError if the build's required board info did not match the device
*/
- protected void verifyRequiredBoards(ITestDevice device, IFlashingResourcesParser resourceParser,
- String deviceProductType) throws TargetSetupError {
+ protected void verifyRequiredBoards(
+ ITestDevice device, IFlashingResourcesParser resourceParser, String deviceProductType)
+ throws TargetSetupError {
if (!containsIgnoreCase(resourceParser.getRequiredBoards(), deviceProductType)) {
throw new TargetSetupError(String.format("Device %s is %s. Expected %s",
device.getSerialNumber(), deviceProductType,
@@ -398,7 +407,10 @@
executeFastbootCmd(
device,
buildFastbootCommand(
- "flash", getBootPartitionName(), bootloaderImageFile.getAbsolutePath()));
+ "flash",
+ mShouldFlashRamdisk,
+ getBootPartitionName(),
+ bootloaderImageFile.getAbsolutePath()));
device.rebootIntoBootloader();
}
@@ -426,8 +438,8 @@
}
/**
- * If needed, flash the baseband image on device. Will only flash baseband if current version
- * on device != required version
+ * If needed, flash the baseband image on device. Will only flash baseband if current version on
+ * device != required version
*
* @param device the {@link ITestDevice} to flash
* @param deviceBuild the {@link IDeviceBuildInfo} that contains the baseband image to flash
@@ -514,7 +526,7 @@
case FLASH_IMG_ZIP:
flashUserDataFromDeviceImageFile(device, deviceBuild);
break;
- case FORCE_WIPE: // intentional fallthrough
+ case FORCE_WIPE: // intentional fallthrough
case WIPE:
CLog.i("Wiping userdata %s", device.getSerialNumber());
wipePartition(device, "userdata");
@@ -544,13 +556,15 @@
/**
* Extracts the userdata.img from device image file and flashes it onto device
+ *
* @param device the {@link ITestDevice} to flash
* @param deviceBuild the {@link IDeviceBuildInfo} that contains the files to flash
* @throws DeviceNotAvailableException if device is not available
* @throws TargetSetupError if failed to extract or flash user data
*/
- protected void flashUserDataFromDeviceImageFile(ITestDevice device,
- IDeviceBuildInfo deviceBuild) throws DeviceNotAvailableException, TargetSetupError {
+ protected void flashUserDataFromDeviceImageFile(
+ ITestDevice device, IDeviceBuildInfo deviceBuild)
+ throws DeviceNotAvailableException, TargetSetupError {
File userdataImg = null;
try {
try (ZipFile zip = new ZipFile(deviceBuild.getDeviceImageFile())) {
@@ -599,16 +613,23 @@
String systemBuildFlavor,
IDeviceBuildInfo deviceBuild)
throws DeviceNotAvailableException, TargetSetupError {
- if (shouldFlashSystem(systemBuildId, systemBuildFlavor, deviceBuild)) {
+ if (shouldFlashSystem(systemBuildId, systemBuildFlavor, deviceBuild)) {
CLog.i("Flashing system %s", deviceBuild.getDeviceBuildId());
flashSystem(device, deviceBuild);
return true;
- }
- CLog.i("System is already version %s and build flavor %s, skipping flashing",
- systemBuildId, systemBuildFlavor);
- // reboot
- device.rebootUntilOnline();
- return false;
+ }
+ CLog.i(
+ "System is already version %s and build flavor %s, skipping flashing",
+ systemBuildId, systemBuildFlavor);
+ if (mShouldFlashRamdisk) {
+ // even if we don't flash system, still flash ramdisk just in case: because the fact
+ // that the system had a different ramdisk won't be captured by a simple build check
+ flashRamdiskIfNeeded(device, deviceBuild);
+ CLog.i("Flashed ramdisk anyways per flasher settings.");
+ }
+ // reboot
+ device.rebootUntilOnline();
+ return false;
}
/**
@@ -655,7 +676,10 @@
executeLongFastbootCmd(
device,
buildFastbootCommand(
- "update", deviceBuild.getDeviceImageFile().getAbsolutePath()));
+ "update",
+ mShouldFlashRamdisk,
+ deviceBuild.getDeviceImageFile().getAbsolutePath()));
+ flashRamdiskIfNeeded(device, deviceBuild);
// only transfer last fastboot command status over to system flash status after having
// flashing the system partitions
mSystemFlashStatus = mFbCmdStatus;
@@ -690,8 +714,9 @@
return matcher.group(1);
} else {
attempts++;
- CLog.w("Could not find version for '%s'. Output '%s', retrying.",
- imageName, queryOutput);
+ CLog.w(
+ "Could not find version for '%s'. Output '%s', retrying.",
+ imageName, queryOutput);
getRunUtil().sleep(RETRY_SLEEP * (attempts - 1)
+ new Random(System.currentTimeMillis()).nextInt(RETRY_SLEEP));
continue;
@@ -740,9 +765,8 @@
*
* @param device the {@link ITestDevice} to execute command on
* @param cmdArgs the arguments to provide to fastboot
- * @return String the stderr output from command if non-empty. Otherwise returns the stdout
- * Some fastboot commands are weird in that they dump output to stderr on success case
- *
+ * @return String the stderr output from command if non-empty. Otherwise returns the stdout Some
+ * fastboot commands are weird in that they dump output to stderr on success case
* @throws DeviceNotAvailableException if device is not available
* @throws TargetSetupError if fastboot command fails
*/
@@ -755,16 +779,15 @@
/**
* Helper method to execute a long-running fastboot command.
- * <p/>
- * Note: Most fastboot commands normally execute within the timeout allowed by
- * {@link ITestDevice#executeFastbootCommand(String...)}. However, when multiple devices are
- * flashing devices at once, fastboot commands can take much longer than normal.
+ *
+ * <p>Note: Most fastboot commands normally execute within the timeout allowed by {@link
+ * ITestDevice#executeFastbootCommand(String...)}. However, when multiple devices are flashing
+ * devices at once, fastboot commands can take much longer than normal.
*
* @param device the {@link ITestDevice} to execute command on
* @param cmdArgs the arguments to provide to fastboot
- * @return String the stderr output from command if non-empty. Otherwise returns the stdout
- * Some fastboot commands are weird in that they dump output to stderr on success case
- *
+ * @return String the stderr output from command if non-empty. Otherwise returns the stdout Some
+ * fastboot commands are weird in that they dump output to stderr on success case
* @throws DeviceNotAvailableException if device is not available
* @throws TargetSetupError if fastboot command fails
*/
@@ -827,9 +850,9 @@
@Override
public void setDataWipeSkipList(Collection<String> dataWipeSkipList) {
if (dataWipeSkipList == null) {
- dataWipeSkipList = new ArrayList<String> ();
+ dataWipeSkipList = new ArrayList<String>();
}
- if(dataWipeSkipList.isEmpty()) {
+ if (dataWipeSkipList.isEmpty()) {
// To maintain backwards compatibility.
// TODO: deprecate and remove.
dataWipeSkipList.add("media");
@@ -852,4 +875,25 @@
public CommandStatus getSystemFlashingStatus() {
return mSystemFlashStatus;
}
+
+ /** {@inheritDoc} */
+ @Override
+ public void setShouldFlashRamdisk(boolean shouldFlashRamdisk) {
+ mShouldFlashRamdisk = shouldFlashRamdisk;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean shouldFlashRamdisk() {
+ return mShouldFlashRamdisk;
+ }
+
+ protected void flashRamdiskIfNeeded(ITestDevice device, IDeviceBuildInfo deviceBuild)
+ throws TargetSetupError, DeviceNotAvailableException {
+ if (mShouldFlashRamdisk) {
+ executeLongFastbootCmd(
+ device, "flash", "boot", deviceBuild.getRamdiskFile().getAbsolutePath());
+ device.reboot();
+ }
+ }
}
diff --git a/src/com/android/tradefed/targetprep/FlashingResourcesParser.java b/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
index 318cf06..b206bc0 100644
--- a/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
+++ b/src/com/android/tradefed/targetprep/FlashingResourcesParser.java
@@ -18,6 +18,8 @@
import com.android.tradefed.command.remote.DeviceDescriptor;
import com.android.tradefed.util.MultiMap;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.BufferedReader;
import java.io.File;
@@ -29,9 +31,7 @@
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
-import java.util.zip.ZipFile;
/**
* A class that parses out required versions of auxiliary image files needed to flash a device.
@@ -256,11 +256,19 @@
*/
static AndroidInfo getBuildRequirements(File deviceImgZipFile,
Map<String, Constraint> constraints) throws TargetSetupError {
+ if (!deviceImgZipFile.exists()) {
+ throw new TargetSetupError(
+ String.format(
+ "Device image zip %s doesn't not exist", deviceImgZipFile.getName()),
+ null,
+ null);
+ }
+
ZipFile deviceZip = null;
BufferedReader infoReader = null;
try {
deviceZip = new ZipFile(deviceImgZipFile);
- ZipEntry androidInfoEntry = deviceZip.getEntry(ANDROID_INFO_FILE_NAME);
+ ZipArchiveEntry androidInfoEntry = deviceZip.getEntry(ANDROID_INFO_FILE_NAME);
if (androidInfoEntry == null) {
DeviceDescriptor nullDescriptor = null;
throw new TargetSetupError(String.format("Could not find %s in device image zip %s",
diff --git a/src/com/android/tradefed/targetprep/IDeviceFlasher.java b/src/com/android/tradefed/targetprep/IDeviceFlasher.java
index c79a09b..5db33d9 100644
--- a/src/com/android/tradefed/targetprep/IDeviceFlasher.java
+++ b/src/com/android/tradefed/targetprep/IDeviceFlasher.java
@@ -119,4 +119,20 @@
*/
public CommandStatus getSystemFlashingStatus();
+ /**
+ * Sets if an additional ramdisk should be flashed after updating device via image zip
+ *
+ * @param shouldFlashRamdisk
+ */
+ public default void setShouldFlashRamdisk(boolean shouldFlashRamdisk) {
+ // Ignore
+ }
+
+ /**
+ * Checks if the flasher is set to have an additional ramdisk should be flashed after updating
+ * device via image zip
+ */
+ public default boolean shouldFlashRamdisk() {
+ return false;
+ }
}
diff --git a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
index e65f9bd..9be70dd 100644
--- a/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparer.java
@@ -23,10 +23,12 @@
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.ApexInfo;
+import com.android.tradefed.device.PackageInfo;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.targetprep.suite.SuiteApkInstaller;
import com.android.tradefed.util.AaptParser;
import com.android.tradefed.util.BundletoolUtil;
+import com.android.tradefed.util.RunUtil;
import com.google.common.annotations.VisibleForTesting;
@@ -54,24 +56,26 @@
private static final String SPLIT_APKS_SUFFIX = ".apks";
private static final String TRAIN_WITH_APEX_INSTALL_OPTION = "install-multi-package";
- private List<ApexInfo> mTestApexInfoList;
- private Set<String> mApkToInstall;
- private List<String> mApkInstalled;
- private List<String> mSplitsInstallArgs;
+ private List<ApexInfo> mTestApexInfoList = new ArrayList<>();
+ private Set<String> mApkToInstall = new LinkedHashSet<>();
+ private List<String> mApkInstalled = new ArrayList<>();
+ private List<String> mSplitsInstallArgs = new ArrayList<>();
private BundletoolUtil mBundletoolUtil;
@Option(name = "bundletool-file-name", description = "The file name of the bundletool jar.")
private String mBundletoolFilename;
+ @Option(
+ name = "apex-staging-wait-time",
+ description = "The time in ms to wait for apex staged session ready.",
+ isTimeVal = true
+ )
+ private long mApexStagingWaitTime = 1 * 60 * 1000;
+
@Override
public void setUp(ITestDevice device, IBuildInfo buildInfo)
throws TargetSetupError, DeviceNotAvailableException {
- mApkInstalled = new ArrayList<>();
- mApkToInstall = new LinkedHashSet<>();
- mTestApexInfoList = new ArrayList<>();
- mSplitsInstallArgs = new ArrayList<>();
-
if (getTestsFileName().isEmpty()) {
CLog.i("No apk/apex module file to install. Skipping.");
return;
@@ -81,31 +85,28 @@
List<String> testAppFileNames = getTestsFileName();
if (containsApks(testAppFileNames)) {
- try {
- installUsingBundleTool(buildInfo, device);
- if (mTestApexInfoList.isEmpty()) {
- CLog.i("No Apex module in the train. Skipping reboot.");
- return;
- } else {
- device.reboot();
- }
- } catch (IOException e) {
- throw new TargetSetupError(
- "Failed to create tmp spec file for device.", device.getDeviceDescriptor());
- }
- } else {
- // Only contain .apk module.
- if (!containsApex(testAppFileNames)) {
- super.installer(device, buildInfo, testAppFileNames);
+ installUsingBundleTool(buildInfo, device);
+ if (mTestApexInfoList.isEmpty()) {
+ CLog.i("No Apex module in the train. Skipping reboot.");
return;
} else {
- // Any kind of combination of apex/apk.
- installer(device, buildInfo, testAppFileNames);
+ RunUtil.getDefault().sleep(mApexStagingWaitTime);
device.reboot();
}
+ } else {
+ installer(device, buildInfo, testAppFileNames);
+ if (containsApex(testAppFileNames)
+ || containsPersistentApk(testAppFileNames, device, buildInfo)) {
+ device.reboot();
+ }
+ if (mTestApexInfoList.isEmpty()) {
+ CLog.i("Train activation succeed.");
+ return;
+ }
}
Set<ApexInfo> activatedApexes = device.getActiveApexes();
+
if (activatedApexes.isEmpty()) {
throw new TargetSetupError(
String.format(
@@ -133,7 +134,7 @@
listApexInfo(failToActivateApex).toString(), device.getSerialNumber()),
device.getDeviceDescriptor());
}
- CLog.i("Installation succeed.");
+ CLog.i("Train activation succeed.");
}
@Override
@@ -158,21 +159,19 @@
// TODO(b/124461631): Remove after ddmlib supports install-multi-package.
@Override
- protected void installer(ITestDevice device, IBuildInfo buildInfo, List<String> appNames)
+ protected void installer(
+ ITestDevice device, IBuildInfo buildInfo, List<String> testAppFileNames)
throws TargetSetupError, DeviceNotAvailableException {
- for (String appFilename : getTestsFileName()) {
- File appFile = getLocalPathForFilename(buildInfo, appFilename, device);
- if (isApex(appFile)) {
- ApexInfo apexInfo = retrieveApexInfo(appFile, device.getDeviceDescriptor());
- mTestApexInfoList.add(apexInfo);
- }
+ if (containsApex(testAppFileNames)) {
+ mTestApexInfoList = collectApexInfoFromApexModules(testAppFileNames, device, buildInfo);
}
- if (appNames.size() > 1) {
- installMultiPackageContainingApex(device, buildInfo, appNames);
- } else {
- // Single apex file install.
- super.installer(device, buildInfo, appNames);
+ if (containsPersistentApk(testAppFileNames, device, buildInfo)) {
+ // When there is a persistent apk in the train, use '--staged' to install full train
+ // Otherwise, do normal install without '--staged'
+ installTrain(device, buildInfo, testAppFileNames, new String[] {"--staged"});
+ return;
}
+ installTrain(device, buildInfo, testAppFileNames, null);
}
/**
@@ -183,14 +182,22 @@
* @param moduleFilenames List of String. The list of filenames of the mainline modules to be
* installed.
*/
- protected void installMultiPackageContainingApex(
- ITestDevice device, IBuildInfo buildInfo, Collection<String> moduleFilenames)
+ protected void installTrain(
+ ITestDevice device,
+ IBuildInfo buildInfo,
+ Collection<String> moduleFilenames,
+ final String[] extraArgs)
throws TargetSetupError, DeviceNotAvailableException {
List<String> apkPackageNames = new ArrayList<>();
List<String> trainInstallCmd = new ArrayList<>();
trainInstallCmd.add(TRAIN_WITH_APEX_INSTALL_OPTION);
+ if (extraArgs != null) {
+ for (String arg : extraArgs) {
+ trainInstallCmd.add(arg);
+ }
+ }
for (String fileName : moduleFilenames) {
File moduleFile = getLocalPathForFilename(buildInfo, fileName, device);
@@ -206,6 +213,11 @@
}
}
String log = device.executeAdbCommand(trainInstallCmd.toArray(new String[0]));
+
+ // Wait until all apexes are fully staged and ready.
+ // TODO: should have adb level solution b/130039562
+ RunUtil.getDefault().sleep(mApexStagingWaitTime);
+
if (log.contains("Success")) {
CLog.d(
"Train is staged successfully. Cmd: %s, Output: %s.",
@@ -227,7 +239,7 @@
* @param buildInfo build artifact information
*/
protected void installUsingBundleTool(IBuildInfo buildInfo, ITestDevice device)
- throws TargetSetupError, DeviceNotAvailableException, IOException {
+ throws TargetSetupError, DeviceNotAvailableException {
File bundletoolJar = getLocalPathForFilename(buildInfo, getBundletoolFileName(), device);
if (bundletoolJar == null) {
throw new TargetSetupError(
@@ -236,7 +248,17 @@
device.getDeviceDescriptor());
}
mBundletoolUtil = new BundletoolUtil(bundletoolJar);
- String deviceSpecFilePath = getBundletoolUtil().generateDeviceSpecFile(device);
+ String deviceSpecFilePath = "";
+ try {
+ deviceSpecFilePath = getBundletoolUtil().generateDeviceSpecFile(device);
+ } catch (IOException e) {
+ throw new TargetSetupError(
+ String.format(
+ " Failed to generate device spec file on %s.",
+ device.getSerialNumber()),
+ e,
+ device.getDeviceDescriptor());
+ }
if (getTestsFileName().size() == 1) {
// Installs single .apks module.
@@ -464,6 +486,64 @@
return splitsArgs;
}
+ /**
+ * Checks if the input files contain any persistent apk.
+ *
+ * @param testAppFileNames The list of the file names of the modules to install
+ * @param device The test device
+ * @param buildInfo build artifact information
+ * @return <code>true</code> if the input files contains a persistent apk module.
+ */
+ protected boolean containsPersistentApk(
+ List<String> testAppFileNames, ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, DeviceNotAvailableException {
+ for (String moduleFileName : testAppFileNames) {
+ if (moduleFileName.endsWith(APK_SUFFIX) &&
+ isPersistentApk(moduleFileName, device, buildInfo)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if an apk is a persistent apk.
+ *
+ * @param filename The apk module file to check
+ * @param device The test device
+ * @param buildInfo build artifact information
+ * @return <code>true</code> if this is a persistent apk module.
+ */
+ protected boolean isPersistentApk(String filename, ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, DeviceNotAvailableException {
+ File moduleFile = getLocalPathForFilename(buildInfo, filename, device);
+ PackageInfo pkgInfo =
+ device.getAppPackageInfo(parsePackageName(moduleFile, device.getDeviceDescriptor()));
+ return pkgInfo.isPersistentApp();
+ }
+
+ /**
+ * Collects apex info from the apex modules for activation check.
+ *
+ * @param testAppFileNames The list of the file names of the modules to install
+ * @param device The test device
+ * @param buildInfo build artifact information
+ * @return a list containing the apexinfo of the apex modules in the input file lists
+ */
+ protected List<ApexInfo> collectApexInfoFromApexModules(
+ List<String> testAppFileNames, ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError {
+ List<ApexInfo> apexInfoList = new ArrayList<>();
+ for (String appFilename : getTestsFileName()) {
+ File appFile = getLocalPathForFilename(buildInfo, appFilename, device);
+ if (isApex(appFile)) {
+ ApexInfo apexInfo = retrieveApexInfo(appFile, device.getDeviceDescriptor());
+ apexInfoList.add(apexInfo);
+ }
+ }
+ return apexInfoList;
+ }
+
@VisibleForTesting
protected String getBundletoolFileName() {
return mBundletoolFilename;
diff --git a/src/com/android/tradefed/targetprep/KeyValueConfigPreparer.java b/src/com/android/tradefed/targetprep/KeyValueConfigPreparer.java
deleted file mode 100644
index 115b257..0000000
--- a/src/com/android/tradefed/targetprep/KeyValueConfigPreparer.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.targetprep;
-
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.ddmlib.IDevice;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-
-/**
- * A {@link ITargetPreparer} which creates and pushes a simple key/value config file to the device.
- */
-@OptionClass(alias = "key-value-config")
-public class KeyValueConfigPreparer extends BaseTargetPreparer {
-
- @Option(name = "path", description = "The path of the config file on the device",
- mandatory = true)
- private String mPath = null;
-
- @Option(name = "config", description = "The key/value pairs of the config")
- private Map<String, String> mKeys = new HashMap<String, String>();
-
- @Option(name = "separator", description = "The separator used between key and value")
- private String mSep = "=";
-
- @Option(name = "interpolate", description = "Interpolate path variable")
- private boolean mInterpolate = false;
-
- /**
- * {@inheritDoc}
- * @throws TargetSetupError
- */
- @Override
- public void setUp(ITestDevice device, IBuildInfo buildInfo) throws DeviceNotAvailableException,
- TargetSetupError {
- if (mPath == null) {
- throw new TargetSetupError("Option path must be specified",
- device.getDeviceDescriptor());
- }
-
- StringBuilder config = new StringBuilder();
-
- for (Entry<String, String> entry : mKeys.entrySet()) {
- config.append(String.format("%s%s%s\n", entry.getKey(), mSep, entry.getValue()));
- }
-
- String content = config.toString();
-
- if (mInterpolate) {
- final String externalStorageString = "${EXTERNAL_STORAGE}";
- if (content.contains(externalStorageString)) {
- final String externalStoragePath =
- device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
- content = content.replace(externalStorageString, externalStoragePath);
- }
- }
-
- device.pushString(content, mPath);
- }
-}
diff --git a/src/com/android/tradefed/targetprep/PushFilePreparer.java b/src/com/android/tradefed/targetprep/PushFilePreparer.java
index dd76cc4..e7ed4da 100644
--- a/src/com/android/tradefed/targetprep/PushFilePreparer.java
+++ b/src/com/android/tradefed/targetprep/PushFilePreparer.java
@@ -41,6 +41,7 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -62,10 +63,11 @@
private IAbi mAbi;
+ @Deprecated
@Option(
name = "push",
description =
- "A push-spec, formatted as "
+ "Deprecated. Please use push-file instead. A push-spec, formatted as "
+ "'/localpath/to/srcfile.txt->/devicepath/to/destfile.txt' "
+ "or '/localpath/to/srcfile.txt->/devicepath/to/destdir/'. "
+ "May be repeated. The local path may be relative to the test cases "
@@ -78,9 +80,10 @@
name = "push-file",
description =
"A push-spec, specifying the local file to the path where it should be pushed on "
- + "device. May be repeated."
+ + "device. May be repeated. If multiple files are configured to be pushed "
+ + "to the same remote path, the latest one will be pushed."
)
- private Map<File, String> mPushFileSpecs = new HashMap<>();
+ private Map<File, String> mPushFileSpecs = new LinkedHashMap<>();
@Option(name="post-push", description=
"A command to run on the device (with `adb shell (yourcommand)`) after all pushes " +
@@ -202,7 +205,28 @@
}
}
}
-
+ // Search top-level matches
+ for (File searchDir : scanDirs) {
+ try {
+ Set<File> allMatch = FileUtil.findFilesObject(searchDir, fileName);
+ if (allMatch.size() > 1) {
+ CLog.d(
+ "Several match for filename '%s', searching for top-level match.",
+ fileName);
+ for (File f : allMatch) {
+ // Bias toward direct child / top level nodes
+ if (f.getParent().equals(searchDir.getAbsolutePath())) {
+ return f;
+ }
+ }
+ } else if (allMatch.size() == 1) {
+ return allMatch.iterator().next();
+ }
+ } catch (IOException e) {
+ CLog.w("Failed to find test files from directory.");
+ }
+ }
+ // Fall-back to searching everything
try {
// Search the full tests dir if no target dir is available.
src = FileUtil.findFile(fileName, null, scanDirs.toArray(new File[] {}));
@@ -224,26 +248,29 @@
if (mRemount) {
device.remountSystemWritable();
}
+
+ Map<String, File> remoteToLocalMapping = new HashMap<>();
for (String pushspec : mPushSpecs) {
String[] pair = pushspec.split("->");
if (pair.length != 2) {
fail(String.format("Invalid pushspec: '%s'", Arrays.asList(pair)), device);
continue;
}
- Log.d(LOG_TAG, String.format("Trying to push local '%s' to remote '%s'", pair[0],
- pair[1]));
- File src = new File(pair[0]);
- String remotePath = pair[1];
- evaluatePushingPair(device, buildInfo, src, remotePath);
+ remoteToLocalMapping.put(pair[1], new File(pair[0]));
}
// Push the file structure
- for (File src : mPushFileSpecs.keySet()) {
- String remotePath = mPushFileSpecs.get(src);
+ for (File local : mPushFileSpecs.keySet()) {
+ remoteToLocalMapping.put(mPushFileSpecs.get(local), local);
+ }
+
+ for (String remotePath : remoteToLocalMapping.keySet()) {
+ File local = remoteToLocalMapping.get(remotePath);
Log.d(
LOG_TAG,
String.format(
- "Trying to push local '%s' to remote '%s'", src.getPath(), remotePath));
- evaluatePushingPair(device, buildInfo, src, remotePath);
+ "Trying to push local '%s' to remote '%s'",
+ local.getPath(), remotePath));
+ evaluatePushingPair(device, buildInfo, local, remotePath);
}
for (String command : mPostPushCommands) {
@@ -272,18 +299,6 @@
}
}
- private void addPushedFile(ITestDevice device, String remotePath) throws TargetSetupError {
- if (mFilesPushed.contains(remotePath)) {
- throw new TargetSetupError(
- String.format(
- "We pushed two files to the %s location. Check "
- + "your configuration of this target_preparer",
- remotePath),
- device.getDeviceDescriptor());
- }
- mFilesPushed.add(remotePath);
- }
-
private void evaluatePushingPair(
ITestDevice device, IBuildInfo buildInfo, File src, String remotePath)
throws TargetSetupError, DeviceNotAvailableException {
@@ -325,7 +340,7 @@
if (deleteContentOnly) {
remotePath += "/*";
}
- addPushedFile(device, remotePath);
+ mFilesPushed.add(remotePath);
}
} else {
if (!device.pushFile(src, remotePath)) {
@@ -335,7 +350,7 @@
device);
return;
} else {
- addPushedFile(device, remotePath);
+ mFilesPushed.add(remotePath);
}
}
}
diff --git a/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java b/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
index 4b50c3d..9f05a65 100644
--- a/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/RunCommandTargetPreparer.java
@@ -141,5 +141,10 @@
}
}
}
+
+ /** Add a command that will be run by the preparer. */
+ public final void addRunCommand(String cmd) {
+ mCommands.add(cmd);
+ }
}
diff --git a/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java b/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
index 836c4a8..54ce9ac 100644
--- a/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/SideloadOtaTargetPreparer.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.targetprep;
-import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
@@ -34,7 +33,7 @@
* TargetSetupError}, and same applies to any OTA sideload error detected.
*/
@OptionClass(alias = "sideload-ota")
-public class SideloadOtaTargetPreparer extends DeviceBuildInfoInjector {
+public class SideloadOtaTargetPreparer extends DeviceUpdateTargetPreparer {
private static final String SIDELOAD_CMD = "sideload";
// the timeout for state transition from sideload finishes to recovery mode, not making this
@@ -52,32 +51,38 @@
// defaults to 10m: assuming USB 2.0 transfer speed, concurrency and some buffer
private long mSideloadTimeout = 10 * 60 * 1000;
- @Option(
- name = "inject-build-info",
- description =
- "whether build info should be injected "
- + "based on device attributes after sideloading"
- )
- private boolean mInjectBuildInfo = true;
-
+ /** {@inheritDoc} */
@Override
- public void setUp(ITestDevice device, IBuildInfo buildInfo)
- throws TargetSetupError, BuildError, DeviceNotAvailableException {
- if (mSideloadOtaPackage == null) {
- CLog.i("No sideload file provided, assuming no-op; skipping ...");
- return;
- }
+ protected File getDeviceUpdateImage() {
+ return mSideloadOtaPackage;
+ }
+
+ /** Reboots the device into sideload mode in preparation */
+ @Override
+ protected void preUpdateActions(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {
device.rebootIntoSideload();
- String filePath = mSideloadOtaPackage.getAbsolutePath();
- CLog.d("Sideloading package from %s ...", filePath);
- device.executeAdbCommand(mSideloadTimeout, SIDELOAD_CMD, filePath);
+ }
+
+ /** Waits for device to transition from sideload to recovery, then reboot to userspace */
+ @Override
+ protected void postUpdateActions(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {
// after applying sideload, device should transition to recovery mode
device.waitForDeviceInRecovery(POST_SIDELOAD_TRANSITION_TIMEOUT);
- // now reboot and wait for the device to become available
+ CLog.i(
+ "Sideloading completed on %s, rebooting and waiting for boot complete.",
+ device.getDeviceDescriptor());
+ // now reboot to userspace
device.reboot();
- // calling this last because we want to inject device side build info after device boots up
- if (mInjectBuildInfo) {
- super.setUp(device, buildInfo);
- }
+ }
+
+ /** Performs the sideload of OTA package */
+ @Override
+ protected void performDeviceUpdate(File deviceUpdateImage, ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {
+ String filePath = getDeviceUpdateImage().getAbsolutePath();
+ CLog.i("Sideloading package from %s onto %s", filePath, device.getSerialNumber());
+ device.executeAdbCommand(mSideloadTimeout, SIDELOAD_CMD, filePath);
}
}
diff --git a/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java b/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
index 891e651..57ba004 100644
--- a/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
+++ b/src/com/android/tradefed/targetprep/SwitchUserTargetPreparer.java
@@ -21,8 +21,10 @@
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.util.UserUtil;
+
+import java.util.Map;
/**
* A {@link ITargetPreparer} that switches to the specified user kind in setUp. By default it
@@ -36,7 +38,7 @@
name = "user-type",
description = "The type of user to switch to before the module run."
)
- private UserUtil.UserType mUserToSwitchTo = UserUtil.UserType.CURRENT;
+ private UserInfo.UserType mUserToSwitchTo = UserInfo.UserType.CURRENT;
private int mPreExecutionCurrentUser;
@@ -45,17 +47,36 @@
throws TargetSetupError, DeviceNotAvailableException {
mPreExecutionCurrentUser = device.getCurrentUser();
+ Map<Integer, UserInfo> userInfos = device.getUserInfos();
- try {
- UserUtil.switchToUserType(device, mUserToSwitchTo);
- } catch (UserUtil.UserSwitchFailedException err) {
- throw new TargetSetupError(
- String.format("Failed switch to user type %s", mUserToSwitchTo),
- err,
- device.getDeviceDescriptor());
+ if (userInfos
+ .get(mPreExecutionCurrentUser)
+ .isUserType(mUserToSwitchTo, mPreExecutionCurrentUser)) {
+ CLog.i(
+ "User %d is already user type %s, no action.",
+ mPreExecutionCurrentUser, mUserToSwitchTo.toString());
+ return;
}
- CLog.d("Successfully switched to user type %s", mUserToSwitchTo);
+ for (UserInfo userInfo : userInfos.values()) {
+ if (userInfo.isUserType(mUserToSwitchTo, mPreExecutionCurrentUser)) {
+ CLog.i(
+ "User %d is user type %s, switching from %d",
+ userInfo.userId(), mUserToSwitchTo.toString(), mPreExecutionCurrentUser);
+ if (!device.switchUser(userInfo.userId())) {
+ throw new TargetSetupError(
+ String.format("Device failed to switch to user %d", userInfo.userId()),
+ device.getDeviceDescriptor());
+ }
+ return;
+ }
+ }
+
+ throw new TargetSetupError(
+ String.format(
+ "Failed switch to user type %s, no user of that type exists",
+ mUserToSwitchTo),
+ device.getDeviceDescriptor());
}
@Override
diff --git a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
index f1b46f7..f1ad19a 100644
--- a/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
+++ b/src/com/android/tradefed/targetprep/TestAppInstallSetup.java
@@ -381,7 +381,7 @@
* Attempt to install a package or split package on the device.
*
* @param device the {@link ITestDevice} to install package
- * @param apkFiles List of Files. If apkFiles contains only one apk file, the app will be
+ * @param appFiles List of Files. If apkFiles contains only one apk file, the app will be
* installed as a whole package with single file. If apkFiles contains more than one name,
* the app will be installed as split apk with multiple files.
*/
diff --git a/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java b/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
index 9867c74..769065a 100644
--- a/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
+++ b/src/com/android/tradefed/targetprep/app/NoApkTestSkipper.java
@@ -15,7 +15,6 @@
*/
package com.android.tradefed.targetprep.app;
-import com.android.tradefed.build.AppBuildInfo;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
@@ -43,12 +42,7 @@
@Override
public void setUp(ITestDevice device, IBuildInfo buildInfo)
throws TargetSetupError, BuildError, DeviceNotAvailableException {
- if (!(buildInfo instanceof AppBuildInfo)) {
- CLog.d("Build info is not a AppBuildInfo skipping.");
- return;
- }
- AppBuildInfo appBuild = (AppBuildInfo) buildInfo;
- if (appBuild.getAppPackageFiles().isEmpty()) {
+ if (buildInfo.getAppPackageFiles().isEmpty()) {
CLog.d("No app to install, skipping the tests");
for (IDeviceConfiguration deviceConfig : mConfiguration.getDeviceConfig()) {
diff --git a/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java b/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
new file mode 100644
index 0000000..174bb9c
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparer.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep.multi;
+
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.CollectingOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
+import com.android.tradefed.util.ZipUtil2;
+import java.io.File;
+import java.io.IOException;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+
+/**
+ * An {@link com.android.tradefed.targetprep.multi.IMultiTargetPreparer} that set up a system
+ * build's images on top of a device build with the Dynamic System Update
+ */
+@OptionClass(alias = "dynamic-system-update")
+public class DynamicSystemPreparer extends BaseMultiTargetPreparer {
+ static final int DSU_MAX_WAIT_SEC = 10 * 60;
+
+ private static final String DEST_PATH = "/sdcard";
+
+ @Option(name = "device-label", description = "the label for the device.")
+ private String mDeviceLabel = "device";
+
+ @Option(
+ name = "system-label",
+ description = "the label for the null-device used to store the system image information."
+ )
+ private String mSystemLabel = "system";
+
+ private boolean isDSURunning(ITestDevice device) throws DeviceNotAvailableException {
+ CollectingOutputReceiver receiver = new CollectingOutputReceiver();
+ device.executeShellCommand("gsi_tool status", receiver);
+ return receiver.getOutput().contains("running");
+ }
+
+ @Override
+ public void setUp(IInvocationContext context)
+ throws TargetSetupError, BuildError, DeviceNotAvailableException {
+
+ ITestDevice device = context.getDevice(mDeviceLabel);
+
+ ITestDevice systemNullDevice = context.getDevice(mSystemLabel);
+ IDeviceBuildInfo systemBuildInfo =
+ (IDeviceBuildInfo) context.getBuildInfo(systemNullDevice);
+
+ File systemImage = null;
+ File systemImageGZ = null;
+
+ ZipFile zipFile = null;
+ try {
+ zipFile = new ZipFile(systemBuildInfo.getDeviceImageFile());
+ systemImage = ZipUtil2.extractFileFromZip(zipFile, "system.img");
+ // The prequest here is the system.img must be an unsparsed image.
+ // Is there any way to detect the actual format and convert it accordingly.
+ systemImageGZ = new File("system.raw.gz");
+ long rawSize = systemImage.length();
+ ZipUtil.gzipFile(systemImage, systemImageGZ);
+ String remotePath = String.format("%s/%s", DEST_PATH, systemImageGZ.getName());
+ CLog.i("Pushing %s to %s", systemImageGZ.getAbsolutePath(), remotePath);
+ if (!device.pushFile(systemImageGZ, remotePath)) {
+ throw new TargetSetupError(
+ String.format(
+ "Failed to push %s to %s", systemImageGZ.getName(), remotePath),
+ device.getDeviceDescriptor());
+ }
+ device.setProperty("persist.sys.fflag.override.settings_dynamic_system", "true");
+
+ String command =
+ "am start-activity "
+ + "-n com.android.dynsystem/com.android.dynsystem.VerificationActivity "
+ + "-a android.os.image.action.START_INSTALL "
+ + "-d file://"
+ + remotePath
+ + " "
+ + "--el KEY_SYSTEM_SIZE "
+ + rawSize
+ + " "
+ + "--el KEY_USERDATA_SIZE 8589934592 "
+ + "--ez KEY_ENABLE_WHEN_COMPLETED true";
+ device.executeShellCommand(command);
+ // Check if device shows as unavailable (as expected after the activity finished).
+ device.waitForDeviceNotAvailable(DSU_MAX_WAIT_SEC * 1000);
+ device.waitForDeviceOnline();
+ // the waitForDeviceOnline may block and we need to correct the 'i'
+ // which is used to measure timeout accordingly
+ if (!isDSURunning(device)) {
+ throw new TargetSetupError("Timeout to boot into DSU");
+ }
+ CommandResult result = device.executeShellV2Command("gsi_tool enable");
+ if (CommandStatus.SUCCESS.equals(result.getStatus())) {
+ // success
+ return;
+ } else {
+ throw new TargetSetupError("fail on gsi_tool enable");
+ }
+ } catch (IOException e) {
+ CLog.e(e);
+ throw new TargetSetupError(
+ "fail to install the DynamicSystemUpdate", e, device.getDeviceDescriptor());
+ } finally {
+ FileUtil.deleteFile(systemImage);
+ FileUtil.deleteFile(systemImageGZ);
+ ZipUtil2.closeZip(zipFile);
+ }
+ }
+
+ @Override
+ public void tearDown(IInvocationContext context, Throwable e)
+ throws DeviceNotAvailableException {
+ if (e instanceof DeviceNotAvailableException) {
+ CLog.e("skip tearDown on DeviceNotAvailableException");
+ return;
+ }
+ ITestDevice device = context.getDevice(mDeviceLabel);
+ // Disable the DynamicSystemUpdate installation
+ device.executeShellCommand("gsi_tool disable");
+ // Enable the one-shot mode when DynamicSystemUpdate is disabled
+ device.executeShellCommand("gsi_tool enable -s");
+ // Disable the DynamicSystemUpdate installation
+ device.executeShellCommand("gsi_tool disable");
+ // Reboot into the original system image
+ device.reboot();
+ }
+}
diff --git a/src/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java b/src/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
new file mode 100644
index 0000000..8e70114
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/multi/MixImageZipPreparer.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep.multi;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IBuildInfo.BuildInfoProperties;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.ZipUtil;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+/** An {@link IMultiTargetPreparer} that mixes a system build's images in a device build. */
+@OptionClass(alias = "mix-image-zip")
+public class MixImageZipPreparer extends BaseMultiTargetPreparer {
+
+ @Option(name = "device-label", description = "the label for the device.")
+ private String mDeviceLabel = "device";
+
+ @Option(
+ name = "system-label",
+ description = "the label for the null-device used to store the system image information."
+ )
+ private String mSystemLabel = "system";
+
+ @Option(
+ name = "resource-label",
+ description = "the label for the null-device used to store the extra build information."
+ )
+ private String mResourceLabel = "resource";
+
+ @Option(
+ name = "extra-build-test-resource-name",
+ description =
+ "the name of the extra build file copied to device build. " + "Can be repeated."
+ )
+ private Set<String> mExtraBuildResourceFiles = new TreeSet<>();
+
+ @Option(
+ name = "system-build-file-name",
+ description =
+ "the name of the image file copied from system build to device build. "
+ + "Can be repeated.",
+ mandatory = true
+ )
+ private Set<String> mSystemFileNames = new TreeSet<>();
+
+ @Option(
+ name = "compression-level",
+ description =
+ "the compression level of the mixed image zip. It is an integer between 0 "
+ + "and 9. Larger value indicates longer time and smaller output."
+ )
+ private int mCompressionLevel = Deflater.DEFAULT_COMPRESSION;
+
+ /** The interface that creates {@link InputStream} from a file or a compressed file. */
+ @VisibleForTesting
+ static interface InputStreamFactory {
+ /** Create a new stream. The caller should close it. */
+ InputStream createInputStream() throws IOException;
+
+ /** Return the uncompressed size of the data. */
+ long getSize();
+
+ /** Return the CRC32 of the data. */
+ long getCrc32() throws IOException;
+ }
+
+ @Override
+ public void setUp(IInvocationContext context)
+ throws TargetSetupError, BuildError, DeviceNotAvailableException {
+
+ ITestDevice device = context.getDevice(mDeviceLabel);
+ IDeviceBuildInfo deviceBuildInfo = (IDeviceBuildInfo) context.getBuildInfo(device);
+
+ ITestDevice systemNullDevice = context.getDevice(mSystemLabel);
+ IDeviceBuildInfo systemBuildInfo =
+ (IDeviceBuildInfo) context.getBuildInfo(systemNullDevice);
+
+ IBuildInfo resourceBuildInfo = null;
+ if (!mExtraBuildResourceFiles.isEmpty()) {
+ ITestDevice resourceNullDevice = context.getDevice(mResourceLabel);
+ resourceBuildInfo = context.getBuildInfo(resourceNullDevice);
+ }
+
+ ZipFile deviceImageZip = null;
+ ZipFile systemImageZip = null;
+ File mixedImageZip = null;
+ try {
+ deviceImageZip = new ZipFile(deviceBuildInfo.getDeviceImageFile());
+
+ // Get all files from device build.
+ Map<String, InputStreamFactory> files =
+ getInputStreamFactoriesFromImageZip(deviceImageZip, file -> true);
+ Map<String, InputStreamFactory> filesNotInDeviceBuild =
+ new HashMap<String, InputStreamFactory>();
+
+ // Get specified files from system build and replace those in device build.
+ systemImageZip = new ZipFile(systemBuildInfo.getDeviceImageFile());
+ Map<String, InputStreamFactory> systemFiles =
+ getInputStreamFactoriesFromImageZip(
+ systemImageZip, file -> mSystemFileNames.contains(file));
+ systemFiles = replaceExistingEntries(systemFiles, files);
+ filesNotInDeviceBuild.putAll(systemFiles);
+
+ if (resourceBuildInfo != null) {
+ // Get specified files from resource build and replace those in device build.
+ Map<String, InputStreamFactory> resourceFiles =
+ getBuildFiles(resourceBuildInfo, mExtraBuildResourceFiles);
+ resourceFiles = replaceExistingEntries(resourceFiles, files);
+ filesNotInDeviceBuild.putAll(resourceFiles);
+ }
+
+ if (!filesNotInDeviceBuild.isEmpty()) {
+ throw new TargetSetupError(
+ String.join(",", filesNotInDeviceBuild.keySet()) + " not in device build.",
+ device.getDeviceDescriptor());
+ }
+
+ CLog.d("Create mixed image zip.");
+ mixedImageZip = createZip(files, mCompressionLevel);
+ } catch (IOException e) {
+ throw new TargetSetupError(
+ "Could not create mixed image zip", e, device.getDeviceDescriptor());
+ } finally {
+ ZipUtil.closeZip(deviceImageZip);
+ ZipUtil.closeZip(systemImageZip);
+ }
+
+ IBuildInfo mixedBuildInfo =
+ createBuildCopy(
+ deviceBuildInfo,
+ systemBuildInfo.getBuildFlavor(),
+ systemBuildInfo.getBuildId(),
+ mixedImageZip);
+ // Replace the build
+ context.addDeviceBuildInfo(mDeviceLabel, mixedBuildInfo);
+ // Clean up the original build
+ deviceBuildInfo.cleanUp();
+ }
+
+ /**
+ * Get {@link InputStreamFactory} from entries in an image zip. The zip must not be closed when
+ * the returned {@link InputStreamFactory} are in use.
+ *
+ * @param zipFile image zip.
+ * @param predicate function that takes a file name as the argument and determines whether the
+ * file name and the content should be added to the output map.
+ * @return map from file name to {@link InputStreamFactory}.
+ * @throws IOException if fails to create the temporary directory.
+ */
+ private static Map<String, InputStreamFactory> getInputStreamFactoriesFromImageZip(
+ final ZipFile zipFile, Predicate<String> predicate) throws IOException {
+ Map<String, InputStreamFactory> factories = new HashMap<String, InputStreamFactory>();
+ Enumeration<? extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ final ZipEntry entry = entries.nextElement();
+ if (entry.isDirectory()) {
+ CLog.w("Image zip contains subdirectory %s.", entry.getName());
+ continue;
+ }
+
+ String name = new File(entry.getName()).getName();
+ if (!predicate.test(name)) {
+ continue;
+ }
+
+ if (entry.getSize() < 0) {
+ throw new IllegalArgumentException("Invalid size.");
+ }
+ if (entry.getCrc() < 0) {
+ throw new IllegalArgumentException("Invalid CRC value.");
+ }
+
+ factories.put(
+ name,
+ new InputStreamFactory() {
+ @Override
+ public InputStream createInputStream() throws IOException {
+ return zipFile.getInputStream(entry);
+ }
+
+ @Override
+ public long getSize() {
+ return entry.getSize();
+ }
+
+ @Override
+ public long getCrc32() {
+ return entry.getCrc();
+ }
+ });
+ }
+ return factories;
+ }
+
+ /**
+ * Get {@link InputStreamFactory} from {@link IBuildInfo} by name.
+ *
+ * @param buildInfo {@link IBuildInfo} that contains files.
+ * @param buildFileNames collection of file names.
+ * @return map from file name to {@link InputStreamFactory}.
+ * @throws IOException if fails to get files from the build info.
+ */
+ private static Map<String, InputStreamFactory> getBuildFiles(
+ IBuildInfo buildInfo, Collection<String> buildFileNames) throws IOException {
+ Map<String, InputStreamFactory> factories = new HashMap<String, InputStreamFactory>();
+ for (String fileName : buildFileNames) {
+ final File file = buildInfo.getFile(fileName);
+ if (file == null) {
+ throw new IOException(String.format("Could not get file with name: %s", fileName));
+ }
+ factories.put(
+ fileName,
+ new InputStreamFactory() {
+ @Override
+ public InputStream createInputStream() throws IOException {
+ return new FileInputStream(file);
+ }
+
+ @Override
+ public long getSize() {
+ return file.length();
+ }
+
+ @Override
+ public long getCrc32() throws IOException {
+ return FileUtil.calculateCrc32(file);
+ }
+ });
+ }
+ return factories;
+ }
+
+ private static void initStoredZipEntry(ZipEntry entry, InputStreamFactory factory)
+ throws IOException {
+ entry.setMethod(ZipOutputStream.STORED);
+ entry.setCompressedSize(factory.getSize());
+ entry.setSize(factory.getSize());
+ entry.setCrc(factory.getCrc32());
+ }
+
+ /**
+ * Create a zip file from {@link InputStreamFactory} instances.
+ *
+ * @param factories the map where the keys are the entry names and the values provide the data
+ * to be compressed.
+ * @param compressionLevel an integer between 0 and 9. If the value is 0, this method creates
+ * {@link ZipOutputStream#STORED} entries instead of default ones.
+ * @return the created zip file in temporary directory.
+ * @throws IOException if any file operation fails.
+ */
+ @VisibleForTesting
+ static File createZip(Map<String, ? extends InputStreamFactory> factories, int compressionLevel)
+ throws IOException {
+ File zipFile = null;
+ OutputStream out = null;
+ try {
+ zipFile = FileUtil.createTempFile("MixedImg", ".zip");
+ out = new FileOutputStream(zipFile);
+ out = new BufferedOutputStream(out);
+ out = new ZipOutputStream(out);
+ ZipOutputStream zipOut = (ZipOutputStream) out;
+ zipOut.setLevel(compressionLevel);
+
+ for (Map.Entry<String, ? extends InputStreamFactory> factory : factories.entrySet()) {
+ ZipEntry entry = new ZipEntry(factory.getKey());
+ // STORED is faster than the default DEFLATED in no compression mode.
+ if (compressionLevel == Deflater.NO_COMPRESSION) {
+ initStoredZipEntry(entry, factory.getValue());
+ }
+ zipOut.putNextEntry(entry);
+ try (InputStream in =
+ new BufferedInputStream(factory.getValue().createInputStream())) {
+ StreamUtil.copyStreams(in, zipOut);
+ }
+ zipOut.closeEntry();
+ }
+
+ File returnValue = zipFile;
+ zipFile = null;
+ return returnValue;
+ } finally {
+ StreamUtil.close(out);
+ FileUtil.deleteFile(zipFile);
+ }
+ }
+
+ private static IBuildInfo createBuildCopy(
+ IDeviceBuildInfo deviceBuildInfo, String buildFlavor, String buildId, File imageZip) {
+ deviceBuildInfo.setProperties(BuildInfoProperties.DO_NOT_COPY_IMAGE_FILE);
+ IDeviceBuildInfo newBuildInfo = (IDeviceBuildInfo) deviceBuildInfo.clone();
+ newBuildInfo.setBuildFlavor(buildFlavor);
+ newBuildInfo.setDeviceImageFile(imageZip, buildId);
+ return newBuildInfo;
+ }
+
+ /**
+ * Replace the values if the keys exists in the map.
+ *
+ * @param replacement the map containing the entries to be added to the original map.
+ * @param original the map whose entries are replaced.
+ * @return the entries which are in the replacement map but not added to the original map.
+ */
+ private static <T> Map<String, T> replaceExistingEntries(
+ Map<String, T> replacement, Map<String, T> original) {
+ Map<String, T> remaining = new HashMap<String, T>();
+ for (Map.Entry<String, T> entry : replacement.entrySet()) {
+ String key = entry.getKey();
+ if (original.containsKey(key)) {
+ original.put(key, entry.getValue());
+ } else {
+ remaining.put(key, entry.getValue());
+ }
+ }
+ return remaining;
+ }
+
+ @VisibleForTesting
+ void addSystemFileName(String fileName) {
+ mSystemFileNames.add(fileName);
+ }
+
+ @VisibleForTesting
+ void addResourceFileName(String fileName) {
+ mExtraBuildResourceFiles.add(fileName);
+ }
+
+ @VisibleForTesting
+ void setCompressionLevel(int compressionLevel) {
+ mCompressionLevel = compressionLevel;
+ }
+}
diff --git a/src/com/android/tradefed/testtype/GTest.java b/src/com/android/tradefed/testtype/GTest.java
index a3a4907..2898afa 100644
--- a/src/com/android/tradefed/testtype/GTest.java
+++ b/src/com/android/tradefed/testtype/GTest.java
@@ -26,6 +26,7 @@
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.NativeCodeCoverageFlusher;
import com.google.common.annotations.VisibleForTesting;
@@ -34,6 +35,7 @@
import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -58,6 +60,24 @@
description = "Stops the Java application runtime before test execution.")
private boolean mStopRuntime = false;
+ @Option(
+ name = "coverage-flush",
+ description = "Forces coverage data to be flushed at the end of the test."
+ )
+ private boolean mCoverageFlush = false;
+
+ @Option(
+ name = "coverage-processes",
+ description = "Name of processes to collect coverage data from."
+ )
+ private List<String> mCoverageProcesses = new ArrayList<>();
+
+ @Option(
+ name = "coverage-clear-before-test",
+ description = "Clears all coverage counters before test execution."
+ )
+ private boolean mCoverageClearBeforeTest = true;
+
// Max characters allowed for executing GTest via command line
private static final int GTEST_CMD_CHAR_LIMIT = 1000;
/**
@@ -68,6 +88,11 @@
mDevice = device;
}
+ @VisibleForTesting
+ void setCoverageProcesses(List<String> coverageProcesses) {
+ mCoverageProcesses = new ArrayList<>(coverageProcesses);
+ }
+
/**
* {@inheritDoc}
*/
@@ -365,14 +390,26 @@
}
// Insert the coverage listener if code coverage collection is enabled.
listener = addNativeCoverageListenerIfEnabled(mDevice, listener);
+ NativeCodeCoverageFlusher flusher = new NativeCodeCoverageFlusher(mDevice);
+
Throwable throwable = null;
try {
+ if (isCoverageEnabled() && mCoverageClearBeforeTest) {
+ if (mCoverageFlush) {
+ flusher.forceCoverageFlush(mCoverageProcesses);
+ }
+ flusher.clearCoverageMeasurements();
+ }
doRunAllTestsInSubdirectory(testPath, mDevice, listener);
} catch (Throwable t) {
throwable = t;
throw t;
} finally {
if (!(throwable instanceof DeviceNotAvailableException)) {
+ if (isCoverageEnabled() && mCoverageFlush) {
+ flusher.forceCoverageFlush(mCoverageProcesses);
+ }
+
if (mStopRuntime) {
mDevice.executeShellCommand("start");
mDevice.waitForDeviceAvailable();
diff --git a/src/com/android/tradefed/testtype/GTestBase.java b/src/com/android/tradefed/testtype/GTestBase.java
index 7766621..eee17c5 100644
--- a/src/com/android/tradefed/testtype/GTestBase.java
+++ b/src/com/android/tradefed/testtype/GTestBase.java
@@ -93,7 +93,7 @@
private long mMaxTestTimeMs = 1 * 60 * 1000L;
@Option(
- name = "native-coverage",
+ name = "coverage",
description =
"Collect code coverage for this test run. Note that the build under test must be a "
+ "coverage build or else this will fail."
@@ -352,6 +352,11 @@
return mIsSharded;
}
+ /** Gets coverage flag. */
+ public boolean isCoverageEnabled() {
+ return mCoverage;
+ }
+
/**
* Define get filter method.
*
diff --git a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
index 412770d..622742e 100644
--- a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
+++ b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
@@ -27,11 +27,8 @@
import com.android.tradefed.device.metric.IMetricCollectorReceiver;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.BugreportCollector;
import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.testdefs.XmlDefsTest;
import com.android.tradefed.util.AbiFormatter;
import com.android.tradefed.util.ListInstrumentationParser;
import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget;
@@ -52,9 +49,6 @@
IMetricCollectorReceiver,
IInvocationContextReceiver {
- /** the metric key name for the test coverage target value */
- // TODO: move this to a more generic location
- public static final String COVERAGE_TARGET_KEY = XmlDefsTest.COVERAGE_TARGET_KEY;
private static final String PM_LIST_CMD = "pm list instrumentation";
private static final String LINE_SEPARATOR = "\\r?\\n";
@@ -94,8 +88,9 @@
"if first device becomes unavailable.")
private boolean mIsResumeMode = false;
- @Option(name = "send-coverage",
- description = "Send coverage target info to test listeners.")
+ /** @deprecated delete when we are sure it's not used anywhere. */
+ @Deprecated
+ @Option(name = "send-coverage", description = "Send coverage target info to test listeners.")
private boolean mSendCoverage = false;
@Option(name = "bugreport-on-failure", description = "Sets which failed testcase events " +
@@ -221,15 +216,6 @@
}
/**
- * Set the send coverage flag.
- * <p/>
- * Exposed for unit testing.
- */
- void setSendCoverage(boolean sendCoverage) {
- mSendCoverage = sendCoverage;
- }
-
- /**
* Gets the list of {@link InstrumentationTest}s contained within.
* <p/>
* Exposed for unit testing.
@@ -342,9 +328,6 @@
CLog.d("Running test %s on %s", test.getPackageName(), getDevice().getSerialNumber());
- if (mSendCoverage && test.getCoverageTarget() != null) {
- sendCoverage(test.getPackageName(), test.getCoverageTarget(), listener);
- }
test.setDevice(getDevice());
if (mTestClass != null) {
test.setClassName(mTestClass);
@@ -363,26 +346,6 @@
}
}
- /**
- * Forwards the tests coverage target info as a test metric.
- *
- * @param packageName
- * @param coverageTarget
- * @param listener
- */
- private void sendCoverage(String packageName, String coverageTarget,
- ITestInvocationListener listener) {
- HashMap<String, Metric> coverageMetric = new HashMap<String, Metric>();
- Metric metric =
- Metric.newBuilder()
- .setMeasurements(
- Measurements.newBuilder().setSingleString(coverageTarget).build())
- .build();
- coverageMetric.put(COVERAGE_TARGET_KEY, metric);
- listener.testRunStarted(packageName, 0);
- listener.testRunEnded(0, coverageMetric);
- }
-
long getShellTimeout() {
return mShellTimeout;
}
diff --git a/src/com/android/tradefed/testtype/InstrumentationTest.java b/src/com/android/tradefed/testtype/InstrumentationTest.java
index f468fdc..aaab43c 100644
--- a/src/com/android/tradefed/testtype/InstrumentationTest.java
+++ b/src/com/android/tradefed/testtype/InstrumentationTest.java
@@ -861,6 +861,7 @@
if (!mIsRerun) {
listener = addBugreportListenerIfEnabled(listener);
listener = addJavaCoverageListenerIfEnabled(listener);
+ listener = addNativeCoverageListenerIfEnabled(listener);
// TODO: Convert to device-side collectors when possible.
for (IMetricCollector collector : mCollectors) {
@@ -927,6 +928,17 @@
}
/**
+ * Returns a listener that will collect native coverage measurements, or the original {@code
+ * listener} if this feature is disabled.
+ */
+ ITestInvocationListener addNativeCoverageListenerIfEnabled(ITestInvocationListener listener) {
+ if (mCoverage) {
+ return new NativeCodeCoverageListener(getDevice(), listener);
+ }
+ return listener;
+ }
+
+ /**
* Execute the test run, but re-run incomplete tests individually if run fails to complete.
*
* @param listener the {@link ITestInvocationListener}
@@ -990,6 +1002,9 @@
}
if (mRebootBeforeReRun) {
mDevice.reboot();
+ } else {
+ // Ensure device is online and responsive before retrying.
+ mDevice.waitForDeviceAvailable();
}
IRemoteTest testReRunner = null;
diff --git a/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java b/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
index b1b6c8d..dca83db 100644
--- a/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
+++ b/src/com/android/tradefed/testtype/NativeCodeCoverageListener.java
@@ -28,6 +28,8 @@
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.ZipUtil;
+import com.google.common.base.Splitter;
+
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@@ -42,7 +44,7 @@
*/
public final class NativeCodeCoverageListener extends ResultForwarder {
- private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace";
+ private static final String NATIVE_COVERAGE_DEVICE_PATH = "/data/misc/trace/proc/self/cwd/out";
private static final String COVERAGE_FILE_LIST_COMMAND =
String.format("find %s -name '*.gcda'", NATIVE_COVERAGE_DEVICE_PATH);
@@ -70,10 +72,12 @@
try {
localDir = FileUtil.createTempDir("native_coverage");
+ // Enable abd root on the device, otherwise the list command will fail.
+ verify(mDevice.enableAdbRoot(), "Failed to enable adb root.");
String findResult = mDevice.executeShellCommand(COVERAGE_FILE_LIST_COMMAND);
Path devicePathRoot = Paths.get(NATIVE_COVERAGE_DEVICE_PATH);
- for (String deviceFile : findResult.split("\n")) {
+ for (String deviceFile : Splitter.on("\n").omitEmptyStrings().split(findResult)) {
// Compute the relative path for the device file.
Path relativePath = devicePathRoot.relativize(Paths.get(deviceFile));
Path localFullPath = localDir.toPath().resolve(relativePath);
diff --git a/src/com/android/tradefed/testtype/NoisyDryRunTest.java b/src/com/android/tradefed/testtype/NoisyDryRunTest.java
index f2ce53c..9d034c7 100644
--- a/src/com/android/tradefed/testtype/NoisyDryRunTest.java
+++ b/src/com/android/tradefed/testtype/NoisyDryRunTest.java
@@ -156,7 +156,7 @@
ConfigurationFactory.getInstance()
.createConfigurationFromArgs(args, null, new DryRunKeyStore());
// Do not resolve dynamic files
- config.validateOptions(false);
+ config.validateOptions();
}
} catch (ConfigurationException e) {
String errorMessage = String.format("Failed to parse command line: %s.", cmdLine);
@@ -182,7 +182,7 @@
.createConfigurationFromArgs(
args, new DryRunKeyStore(), createSandbox(), createRunUtil());
// Do not resolve dynamic files
- config.validateOptions(false);
+ config.validateOptions();
}
/** Returns a {@link IRunUtil} implementation. */
diff --git a/src/com/android/tradefed/testtype/VersionedTfLauncher.java b/src/com/android/tradefed/testtype/VersionedTfLauncher.java
deleted file mode 100644
index 798934a..0000000
--- a/src/com/android/tradefed/testtype/VersionedTfLauncher.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype;
-
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionCopier;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.device.NullDevice;
-import com.android.tradefed.device.StubDevice;
-import com.android.tradefed.util.StringEscapeUtils;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-/**
- * A {@link IRemoteTest} for running tests against a separate TF installation.
- *
- * <p>Launches an external java process to run the tests. Used for running the TF unit or functional
- * tests continuously.
- */
-public class VersionedTfLauncher extends SubprocessTfLauncher
- implements IMultiDeviceTest, IShardableTest {
-
- @Option(name = "tf-command-line", description = "The list string of original command line "
- + "arguments.")
- private List<String> mTfCommandline = new ArrayList<>();
-
- private Map<ITestDevice, IBuildInfo> mDeviceInfos = null;
-
- private int mShardCount = -1;
-
- private int mShardIndex = -1;
-
- public VersionedTfLauncher() {
- super();
- }
-
- private VersionedTfLauncher(int shardCount, int shardIndex) {
- this();
- mShardCount = shardCount;
- mShardIndex = shardIndex;
- }
-
- /**
- * {@inheritDoc}
- * <p/>
- * The method tokenizes the command line arguments specified by --tf-command-line, and
- * appends the arguments to the subprocess of TF run. It also passes in the serial of the test
- * device through --serial option, to force the subprocess to use the device selected by the
- * parent TF process.
- */
- @Override
- protected void preRun() {
- super.preRun();
-
- if (!mTfCommandline.isEmpty()) {
- mCmdArgs.addAll(StringEscapeUtils.paramsToArgs(mTfCommandline));
- }
-
- // TODO: support multiple device test.
- if (mDeviceInfos == null || mDeviceInfos.size() == 0) {
- throw new RuntimeException("Device is not allocated for the test.");
- } else if (mDeviceInfos.size() > 1) {
- throw new RuntimeException("More than one devices are allocated for the test.");
- } else {
- ITestDevice device = mDeviceInfos.entrySet().iterator().next().getKey();
- if (device.getIDevice() instanceof NullDevice) {
- mCmdArgs.add("--null-device");
- } else if (!(device.getIDevice() instanceof StubDevice)) {
- String serial = device.getSerialNumber();
- mCmdArgs.add("--serial");
- mCmdArgs.add(serial);
- }
- }
-
- if (0 <= mShardCount && 0 <= mShardIndex) {
- mCmdArgs.add("--shard-count");
- mCmdArgs.add(Integer.toString(mShardCount));
- mCmdArgs.add("--shard-index");
- mCmdArgs.add(Integer.toString(mShardIndex));
- }
-
- // Add option for the path to general-tests.zip
- File generalTests = mBuildInfo.getFile("general-tests.zip");
- if (generalTests != null) {
- mCmdArgs.add("--additional-tests-zip");
- mCmdArgs.add(generalTests.getAbsolutePath());
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void setDeviceInfos(Map<ITestDevice, IBuildInfo> deviceInfos) {
- mDeviceInfos = deviceInfos;
- }
-
- @Override
- public Collection<IRemoteTest> split(int shardCountHint) {
- if (shardCountHint <= 1) {
- return null;
- }
- Collection<IRemoteTest> tests = new ArrayList<>();
- for (int i = 0; i < shardCountHint; i++) {
- tests.add(getTestShard(shardCountHint, i));
- }
- return tests;
- }
-
- private IRemoteTest getTestShard(int shardCount, int shardIndex) {
- IRemoteTest shard = new VersionedTfLauncher(shardCount, shardIndex);
- try {
- OptionCopier.copyOptions(this, shard);
- } catch (ConfigurationException e) {
- // Bail out rather than run tests with unexpected options
- throw new RuntimeException("failed to copy options", e);
- }
- return shard;
- }
-
-}
diff --git a/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java b/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
index e77ae62..63649fb 100644
--- a/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
+++ b/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4Test.java
@@ -43,6 +43,8 @@
import com.android.tradefed.util.ListInstrumentationParser;
import com.android.tradefed.util.ListInstrumentationParser.InstrumentationTarget;
+import com.google.common.base.Joiner;
+
import org.junit.After;
import org.junit.Assume;
@@ -52,6 +54,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
/**
* Base test class for running host JUnit4 style tests. This class provides support to install, run
@@ -601,7 +604,13 @@
throw new AssertionError(errorBuilder.toString());
}
// Assume not all tests have skipped (and rethrow AssumptionViolatedException if so)
+ List<TestResult> assumpFail =
+ runResult.getTestsResultsInState(TestStatus.ASSUMPTION_FAILURE);
+ List<String> messages =
+ assumpFail.stream().map(r -> r.getStackTrace()).collect(Collectors.toList());
+ String errors = Joiner.on("\n\n").join(messages);
Assume.assumeTrue(
+ errors,
runResult.getNumTests()
!= runResult.getNumTestsInState(TestStatus.ASSUMPTION_FAILURE));
}
diff --git a/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java b/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java
new file mode 100644
index 0000000..862dd36
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/BaseRetryDecision.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.testtype.IRemoteTest;
+import com.android.tradefed.testtype.ITestFilterReceiver;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Base implementation of {@link IRetryDecision}. Base implementation only take local signals into
+ * account.
+ */
+public class BaseRetryDecision implements IRetryDecision {
+
+ private RetryStrategy mRetryStrategy;
+ private IRemoteTest mCurrentlyConsideredTest;
+ private RetryStatsHelper mStatistics;
+
+ /** Constructor for the retry decision, always based on the {@link RetryStrategy}. */
+ public BaseRetryDecision(RetryStrategy strategy) {
+ mRetryStrategy = strategy;
+ }
+
+ @Override
+ public boolean shouldRetry(IRemoteTest test, List<TestRunResult> previousResults) {
+ // Keep track of some results for the test in progress for statistics purpose.
+ if (test != mCurrentlyConsideredTest) {
+ mCurrentlyConsideredTest = test;
+ mStatistics = new RetryStatsHelper();
+ }
+
+ switch (mRetryStrategy) {
+ case NO_RETRY:
+ // Return directly if we are not considering retry at all.
+ return false;
+ case ITERATIONS:
+ // For iterations, retry directly, we have nothing to setup
+ return true;
+ case RERUN_UNTIL_FAILURE:
+ // For retrying until failure, if any failures occurred, skip retry.
+ return !hasAnyFailures(previousResults);
+ default:
+ // Continue the logic for retry the failures.
+ break;
+ }
+
+ mStatistics.addResultsFromRun(previousResults);
+ if (!(test instanceof ITestFilterReceiver)) {
+ CLog.d(
+ "%s does not implement ITestFilterReceiver, thus cannot work with auto-retry.",
+ test);
+ return false;
+ }
+
+ // TODO(b/77548917): Right now we only support ITestFilterReceiver. We should expect to
+ // support ITestFile*Filter*Receiver in the future.
+ ITestFilterReceiver filterableTest = (ITestFilterReceiver) test;
+ return handleRetryFailures(filterableTest, previousResults);
+ }
+
+ @Override
+ public void addLastAttempt(List<TestRunResult> lastResults) {
+ mStatistics.addResultsFromRun(lastResults);
+ }
+
+ @Override
+ public RetryStatistics getRetryStats() {
+ return mStatistics.calculateStatistics();
+ }
+
+ /** Returns the set of failed test cases that should be retried. */
+ public static Set<TestDescription> getFailedTestCases(List<TestRunResult> previousResults) {
+ Set<TestDescription> failedTestCases = new HashSet<TestDescription>();
+ for (TestRunResult run : previousResults) {
+ if (run != null) {
+ failedTestCases.addAll(run.getFailedTests());
+ }
+ }
+ return failedTestCases;
+ }
+
+ private boolean handleRetryFailures(
+ ITestFilterReceiver test, List<TestRunResult> previousResults) {
+ if (hasRunFailures(previousResults)) {
+ return true;
+ }
+
+ // In case of test case failure, we retry with filters.
+ Set<TestDescription> previousFailedTests = getFailedTestCases(previousResults);
+ if (!previousFailedTests.isEmpty()) {
+ CLog.d("Retrying the test case failure.");
+ addRetriedTestsToIncludeFilters(test, previousFailedTests);
+ return true;
+ }
+
+ CLog.d("No test run or test case failures. No need to retry.");
+ return false;
+ }
+
+ /** Returns true if there are any failures in the previous results. */
+ private boolean hasAnyFailures(List<TestRunResult> previousResults) {
+ for (TestRunResult run : previousResults) {
+ if (run != null && (run.isRunFailure() || run.hasFailedTests())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns true if there are any run failures in the previous results. */
+ private boolean hasRunFailures(List<TestRunResult> previousResults) {
+ for (TestRunResult run : previousResults) {
+ if (run != null && run.isRunFailure()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Set the filters on the test runner for the retry. */
+ private void addRetriedTestsToIncludeFilters(
+ ITestFilterReceiver test, Set<TestDescription> testDescriptions) {
+ // Limit the re-run to the failure we include, so clear filters then put our failures
+ test.clearIncludeFilters();
+ for (TestDescription testCase : testDescriptions) {
+ String filter = testCase.toString();
+ test.addIncludeFilter(filter);
+ }
+ }
+}
diff --git a/src/com/android/tradefed/testtype/retry/IRetryDecision.java b/src/com/android/tradefed/testtype/retry/IRetryDecision.java
new file mode 100644
index 0000000..808520f
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/IRetryDecision.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.testtype.IRemoteTest;
+
+import java.util.List;
+
+/**
+ * Interface driving the retry decision and applying the filter on the class for more targeted
+ * retry.
+ */
+public interface IRetryDecision {
+
+ /**
+ * Decide whether or not retry should be attempted. Also make any necessary changes to the
+ * {@link IRemoteTest} to be retried (Applying filters, etc.).
+ *
+ * @param test The {@link IRemoteTest} that just ran.
+ * @param previousResults The list of {@link TestRunResult} of the test that just ran.
+ * @return True if we should retry, False otherwise.
+ */
+ public boolean shouldRetry(IRemoteTest test, List<TestRunResult> previousResults);
+
+ /**
+ * {@link #shouldRetry(IRemoteTest, List)} will most likely be called before the last retry
+ * attempt, so we might be missing the very last attempt results for statistics purpose. This
+ * method allows those results to be provided for proper statistics calculations.
+ *
+ * @param lastResults
+ */
+ public void addLastAttempt(List<TestRunResult> lastResults);
+
+ /** Returns the {@link RetryStatistics} representing the retry. */
+ public RetryStatistics getRetryStats();
+}
diff --git a/src/com/android/tradefed/testtype/retry/MergeStrategy.java b/src/com/android/tradefed/testtype/retry/MergeStrategy.java
new file mode 100644
index 0000000..d8c6869
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/MergeStrategy.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+/** Describes how the results should be aggregated when multiple attempts are present. */
+public enum MergeStrategy {
+ /** Merging should not be applied and will throw an exception. */
+ NO_MERGE,
+ /** If a single test case pass then we will consider the merged result passed. */
+ ONE_TESTCASE_PASS_IS_PASS,
+ /** If a single test run pass then we will consider the merged run result passed. */
+ ONE_TESTRUN_PASS_IS_PASS,
+ /** If a single run or test cases is a pass we will consider the merged results passed. */
+ ANY_PASS_IS_PASS,
+ /** If a single run or test cases is failed, status will be failed no matter what. */
+ ANY_FAIL_IS_FAIL;
+
+ /** Create a merge strategy based on the retry strategy. */
+ public static MergeStrategy getMergeStrategy(RetryStrategy retryStrategy) {
+ // TODO: Expand to take into account more context: postsubmit vs. presubmit
+ MergeStrategy strategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
+ switch (retryStrategy) {
+ case ITERATIONS:
+ strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
+ break;
+ case RERUN_UNTIL_FAILURE:
+ strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
+ break;
+ case RETRY_ANY_FAILURE:
+ strategy = MergeStrategy.ANY_PASS_IS_PASS;
+ break;
+ case NO_RETRY:
+ strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
+ break;
+ }
+ return strategy;
+ }
+}
diff --git a/src/com/android/tradefed/testtype/retry/ResultAggregator.java b/src/com/android/tradefed/testtype/retry/ResultAggregator.java
new file mode 100644
index 0000000..b8ed0bc
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/ResultAggregator.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.ILogSaver;
+import com.android.tradefed.result.ILogSaverListener;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.LogFile;
+import com.android.tradefed.result.ResultAndLogForwarder;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestResult;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.result.retry.ISupportGranularResults;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Special forwarder that aggregates the results when needed, based on the retry strategy that was
+ * taken.
+ */
+public class ResultAggregator extends CollectingTestListener {
+
+ /* Forwarder to ALL result reporters */
+ private ResultAndLogForwarder mAllForwarder;
+ /* Forwarder to result reporters that only support aggregated results */
+ private ResultAndLogForwarder mAggregatedForwarder;
+ /* Forwarder to result reporters that support the attempt reporting */
+ private ResultAndLogForwarder mDetailedForwarder;
+ private RetryStrategy mRetryStrategy;
+ // Track whether or not a module was started.
+ private boolean mModuleInProgress = false;
+ // Stores the results from non-module test runs until they are ready to be replayed.
+ private List<TestRunResult> mPureRunResults = new ArrayList<>();
+
+ public ResultAggregator(List<ITestInvocationListener> listeners, RetryStrategy strategy) {
+ mAllForwarder = new ResultAndLogForwarder(listeners);
+
+ List<ITestInvocationListener> supportDetails =
+ listeners
+ .stream()
+ .filter(
+ i ->
+ ((i instanceof ISupportGranularResults)
+ && ((ISupportGranularResults) i)
+ .supportGranularResults()))
+ .collect(Collectors.toList());
+ List<ITestInvocationListener> noSupportDetails =
+ listeners
+ .stream()
+ .filter(
+ i ->
+ !(i instanceof ISupportGranularResults)
+ || !((ISupportGranularResults) i)
+ .supportGranularResults())
+ .collect(Collectors.toList());
+
+ mAggregatedForwarder = new ResultAndLogForwarder(noSupportDetails);
+ mDetailedForwarder = new ResultAndLogForwarder(supportDetails);
+
+ mRetryStrategy = strategy;
+ MergeStrategy mergeStrategy = MergeStrategy.getMergeStrategy(mRetryStrategy);
+ setMergeStrategy(mergeStrategy);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void invocationStarted(IInvocationContext context) {
+ super.invocationStarted(context);
+ mAllForwarder.invocationStarted(context);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void invocationFailed(Throwable cause) {
+ super.invocationFailed(cause);
+ mAllForwarder.invocationFailed(cause);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void invocationEnded(long elapsedTime) {
+ if (!mPureRunResults.isEmpty()) {
+ forwardTestRunResults(mPureRunResults, mAggregatedForwarder);
+ mPureRunResults.clear();
+ }
+ super.invocationEnded(elapsedTime);
+ mAllForwarder.invocationEnded(elapsedTime);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void testModuleStarted(IInvocationContext moduleContext) {
+ if (!mPureRunResults.isEmpty()) {
+ forwardTestRunResults(mPureRunResults, mAggregatedForwarder);
+ mPureRunResults.clear();
+ }
+
+ mModuleInProgress = true;
+ super.testModuleStarted(moduleContext);
+ mAllForwarder.testModuleStarted(moduleContext);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setLogSaver(ILogSaver logSaver) {
+ super.setLogSaver(logSaver);
+ mAllForwarder.setLogSaver(logSaver);
+ }
+
+ // ====== Forwarders to the detailed result reporters
+
+ @Override
+ public void testRunStarted(String name, int testCount, int attemptNumber, long startTime) {
+ if (!mPureRunResults.isEmpty() && !mPureRunResults.get(0).getName().equals(name)) {
+ forwardTestRunResults(mPureRunResults, mAggregatedForwarder);
+ mPureRunResults.clear();
+ }
+ super.testRunStarted(name, testCount, attemptNumber, startTime);
+ mDetailedForwarder.testRunStarted(name, testCount, attemptNumber, startTime);
+ }
+
+ @Override
+ public void testRunFailed(String errorMessage) {
+ super.testRunFailed(errorMessage);
+ mDetailedForwarder.testRunFailed(errorMessage);
+ }
+
+ @Override
+ public void testStarted(TestDescription test, long startTime) {
+ super.testStarted(test, startTime);
+ mDetailedForwarder.testStarted(test, startTime);
+ }
+
+ @Override
+ public void testIgnored(TestDescription test) {
+ super.testIgnored(test);
+ mDetailedForwarder.testIgnored(test);
+ }
+
+ @Override
+ public void testAssumptionFailure(TestDescription test, String trace) {
+ super.testAssumptionFailure(test, trace);
+ mDetailedForwarder.testAssumptionFailure(test, trace);
+ }
+
+ @Override
+ public void testFailed(TestDescription test, String trace) {
+ super.testFailed(test, trace);
+ mDetailedForwarder.testFailed(test, trace);
+ }
+
+ @Override
+ public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) {
+ super.testEnded(test, endTime, testMetrics);
+ mDetailedForwarder.testEnded(test, endTime, testMetrics);
+ }
+
+ @Override
+ public void logAssociation(String dataName, LogFile logFile) {
+ super.logAssociation(dataName, logFile);
+ mDetailedForwarder.logAssociation(dataName, logFile);
+ }
+
+ @Override
+ public void testLogSaved(
+ String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile) {
+ super.testLogSaved(dataName, dataType, dataStream, logFile);
+ mDetailedForwarder.testLogSaved(dataName, dataType, dataStream, logFile);
+ }
+
+ // ===== Forwarders to the aggregated reporters.
+
+ @Override
+ public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) {
+ super.testRunEnded(elapsedTime, runMetrics);
+ mDetailedForwarder.testRunEnded(elapsedTime, runMetrics);
+
+ // If we are not a module and we reach here. This allows to support non-suite scenarios
+ if (!mModuleInProgress) {
+ // We can't forward yet otherwise we might not have aggregated simple runs.
+ mPureRunResults.add(getCurrentRunResults());
+ }
+ }
+
+ @Override
+ public void testModuleEnded() {
+ mModuleInProgress = false;
+ super.testModuleEnded();
+ // We still forward the testModuleEnd to the detailed reporters
+ mDetailedForwarder.testModuleEnded();
+
+ List<TestRunResult> mergedResults = getMergedTestRunResults();
+ Set<String> resultNames = new HashSet<>();
+ int expectedTestCount = 0;
+ for (TestRunResult result : mergedResults) {
+ expectedTestCount += result.getExpectedTestCount();
+ resultNames.add(result.getName());
+ }
+
+ // Forward all the results aggregated
+ mAggregatedForwarder.testRunStarted(
+ getCurrentRunResults().getName(),
+ expectedTestCount,
+ /* Attempt*/ 0,
+ /* Start Time */ getCurrentRunResults().getStartTime());
+ for (TestRunResult runResult : mergedResults) {
+ forwardTestResults(runResult.getTestResults(), mAggregatedForwarder);
+ if (runResult.isRunFailure()) {
+ mAggregatedForwarder.testRunFailed(runResult.getRunFailureMessage());
+ }
+ }
+ // Provide a strong association of the run to its logs.
+ for (Entry<String, LogFile> logFile :
+ getCurrentRunResults().getRunLoggedFiles().entrySet()) {
+ mAggregatedForwarder.logAssociation(logFile.getKey(), logFile.getValue());
+ }
+ mAggregatedForwarder.testRunEnded(
+ getCurrentRunResults().getElapsedTime(),
+ getCurrentRunResults().getRunProtoMetrics());
+ mAggregatedForwarder.testModuleEnded();
+ // Ensure we don't carry results from one module to another.
+ for (String name : resultNames) {
+ clearResultsForName(name);
+ }
+ }
+
+ private void forwardTestResults(
+ Map<TestDescription, TestResult> testResults, ITestInvocationListener listener) {
+ for (Map.Entry<TestDescription, TestResult> testEntry : testResults.entrySet()) {
+ listener.testStarted(testEntry.getKey(), testEntry.getValue().getStartTime());
+ switch (testEntry.getValue().getStatus()) {
+ case FAILURE:
+ listener.testFailed(testEntry.getKey(), testEntry.getValue().getStackTrace());
+ break;
+ case ASSUMPTION_FAILURE:
+ listener.testAssumptionFailure(
+ testEntry.getKey(), testEntry.getValue().getStackTrace());
+ break;
+ case IGNORED:
+ listener.testIgnored(testEntry.getKey());
+ break;
+ case INCOMPLETE:
+ listener.testFailed(
+ testEntry.getKey(), "Test did not complete due to exception.");
+ break;
+ default:
+ break;
+ }
+ // Provide a strong association of the test to its logs.
+ for (Entry<String, LogFile> logFile :
+ testEntry.getValue().getLoggedFiles().entrySet()) {
+ if (listener instanceof ILogSaverListener) {
+ ((ILogSaverListener) listener)
+ .logAssociation(logFile.getKey(), logFile.getValue());
+ }
+ }
+ listener.testEnded(
+ testEntry.getKey(),
+ testEntry.getValue().getEndTime(),
+ testEntry.getValue().getProtoMetrics());
+ }
+ }
+
+ /**
+ * Helper method to forward the results from multiple attempts of the same Test Run (same name).
+ */
+ private void forwardTestRunResults(List<TestRunResult> results, ILogSaverListener listener) {
+ TestRunResult result =
+ TestRunResult.merge(results, MergeStrategy.getMergeStrategy(mRetryStrategy));
+
+ listener.testRunStarted(
+ result.getName(), result.getExpectedTestCount(), 0, result.getStartTime());
+ forwardTestResults(result.getTestResults(), listener);
+ if (result.isRunFailure()) {
+ listener.testRunFailed(result.getRunFailureMessage());
+ }
+ // Provide a strong association of the run to its logs.
+ for (Entry<String, LogFile> logFile : result.getRunLoggedFiles().entrySet()) {
+ listener.logAssociation(logFile.getKey(), logFile.getValue());
+ }
+ listener.testRunEnded(result.getElapsedTime(), result.getRunProtoMetrics());
+ // Ensure we don't keep track of the results we just forwarded
+ clearResultsForName(result.getName());
+ }
+}
diff --git a/src/com/android/tradefed/testtype/retry/RetryStatistics.java b/src/com/android/tradefed/testtype/retry/RetryStatistics.java
new file mode 100644
index 0000000..4417928
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/RetryStatistics.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.testtype.IRemoteTest;
+
+import java.util.List;
+
+/**
+ * Structure holding the statistics for a retry session of one {@link IRemoteTest}. Not all fields
+ * might be populated depending of the {@link RetryStrategy}.
+ */
+public class RetryStatistics {
+ // The time spent in retry. Always populated if retries or iterations occurred
+ public long mRetryTime = 0L;
+
+ // Success and failure counts. Populated for RETRY_ANY_FAILURE.
+ public long mRetrySuccess = 0L;
+ public long mRetryFailure = 0L;
+
+ /** Helper method to aggregate the statistics of several retries. */
+ public static final RetryStatistics aggregateStatistics(List<RetryStatistics> stats) {
+ RetryStatistics aggregatedStats = new RetryStatistics();
+ for (RetryStatistics s : stats) {
+ aggregatedStats.mRetryTime += s.mRetryTime;
+ aggregatedStats.mRetrySuccess += s.mRetrySuccess;
+ aggregatedStats.mRetryFailure += s.mRetryFailure;
+ }
+ return aggregatedStats;
+ }
+}
diff --git a/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java b/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java
new file mode 100644
index 0000000..c9748ce
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/RetryStatsHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestRunResult;
+
+import com.google.common.collect.Sets;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/** Calculate the retry statistics and metrics based on attempts comparison. */
+public class RetryStatsHelper {
+
+ private List<List<TestRunResult>> mResults = new ArrayList<>();
+ private RetryStatistics mStats = new RetryStatistics();
+
+ /** Add the results from the latest run to be tracked for statistics purpose. */
+ public void addResultsFromRun(List<TestRunResult> mLatestResults) {
+ if (!mResults.isEmpty()) {
+ updateSuccess(mResults.get(mResults.size() - 1), mLatestResults);
+ }
+ mResults.add(mLatestResults);
+ }
+
+ /**
+ * Calculate the retry statistics based on currently known results and return the associated
+ * {@link RetryStatistics} to represent the results.
+ */
+ public RetryStatistics calculateStatistics() {
+ if (!mResults.isEmpty()) {
+ List<TestRunResult> attemptResults = mResults.get(mResults.size() - 1);
+ Set<TestDescription> attemptFailures =
+ BaseRetryDecision.getFailedTestCases(attemptResults);
+ mStats.mRetryFailure = attemptFailures.size();
+ }
+ return mStats;
+ }
+
+ private void updateSuccess(
+ List<TestRunResult> previousResults, List<TestRunResult> latestResults) {
+ Set<TestDescription> diff =
+ Sets.difference(
+ BaseRetryDecision.getFailedTestCases(previousResults),
+ BaseRetryDecision.getFailedTestCases(latestResults));
+ mStats.mRetrySuccess += diff.size();
+ }
+}
diff --git a/src/com/android/tradefed/testtype/retry/RetryStrategy.java b/src/com/android/tradefed/testtype/retry/RetryStrategy.java
new file mode 100644
index 0000000..dcce258
--- /dev/null
+++ b/src/com/android/tradefed/testtype/retry/RetryStrategy.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+/** The Retry Strategy to be used when re-running some tests. */
+public enum RetryStrategy {
+ /** Do not attempt any retry */
+ NO_RETRY,
+ /** Rerun all the tests for the number of attempts specified. */
+ ITERATIONS,
+ /**
+ * Rerun all the tests until the max count is reached or a failure occurs whichever come first.
+ */
+ RERUN_UNTIL_FAILURE,
+ /**
+ * Rerun all the test run and test cases failures until passed or the max number of attempts
+ * specified. Test run failures are rerun in priority (a.k.a. if a run failure and a test case
+ * failure occur, the run failure is rerun).
+ */
+ RETRY_ANY_FAILURE,
+}
diff --git a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
index e66b850..9c01b43 100644
--- a/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/BaseTestSuite.java
@@ -17,22 +17,25 @@
import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.build.IDeviceBuildInfo;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.LogDataType;
import com.android.tradefed.testtype.IAbi;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.suite.params.ModuleParameters;
import com.android.tradefed.util.ArrayUtil;
+import com.android.tradefed.util.FileUtil;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileNotFoundException;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -53,6 +56,7 @@
public static final char TEST_OPTION_SHORT_NAME = 't';
public static final String CONFIG_PATTERNS_OPTION = "config-patterns";
private static final String MODULE_ARG_OPTION = "module-arg";
+ private static final int MAX_FILTER_DISPLAY = 20;
@Option(
name = INCLUDE_FILTER_OPTION,
@@ -148,6 +152,14 @@
private boolean mEnableParameter = false;
@Option(
+ name = "enable-optional-parameterization",
+ description =
+ "Whether or not to enable optional parameters. Optional parameters are "
+ + "parameters not usually used by default."
+ )
+ private boolean mEnableOptionalParameter = false;
+
+ @Option(
name = "module-parameter",
description =
"Allows to run only one module parameter type instead of all the combinations. "
@@ -181,14 +193,62 @@
SuiteModuleLoader.addFilters(mIncludeFilters, mIncludeFiltersParsed, abis);
SuiteModuleLoader.addFilters(mExcludeFilters, mExcludeFiltersParsed, abis);
+ String includeFilter = mIncludeFiltersParsed.toString();
+ if (mIncludeFiltersParsed.size() > MAX_FILTER_DISPLAY) {
+ if (isSplitting()) {
+ includeFilter = includeFilter.substring(0, 100) + "...";
+ } else {
+ File suiteIncludeFilters = null;
+ try {
+ suiteIncludeFilters =
+ FileUtil.createTempFile("suite-include-filters", ".txt");
+ FileUtil.writeToFile(mIncludeFiltersParsed.toString(), suiteIncludeFilters);
+ logFilterFile(
+ suiteIncludeFilters,
+ suiteIncludeFilters.getName(),
+ LogDataType.TEXT);
+ includeFilter = String.format("See %s", suiteIncludeFilters.getName());
+ } catch (IOException e) {
+ CLog.e(e);
+ } finally {
+ FileUtil.deleteFile(suiteIncludeFilters);
+ }
+ }
+ }
+
+ String excludeFilter = mExcludeFiltersParsed.toString();
+ if (mExcludeFiltersParsed.size() > MAX_FILTER_DISPLAY) {
+ if (isSplitting()) {
+ excludeFilter = excludeFilter.substring(0, 100) + "...";
+ } else {
+ File suiteExcludeFilters = null;
+ try {
+ suiteExcludeFilters =
+ FileUtil.createTempFile("suite-exclude-filters", ".txt");
+ FileUtil.writeToFile(mExcludeFiltersParsed.toString(), suiteExcludeFilters);
+ logFilterFile(
+ suiteExcludeFilters,
+ suiteExcludeFilters.getName(),
+ LogDataType.TEXT);
+ excludeFilter = String.format("See %s", suiteExcludeFilters.getName());
+ } catch (IOException e) {
+ CLog.e(e);
+ } finally {
+ FileUtil.deleteFile(suiteExcludeFilters);
+ }
+ }
+ }
+
CLog.d(
"Initializing ModuleRepo\nABIs:%s\n"
+ "Test Args:%s\nModule Args:%s\nIncludes:%s\nExcludes:%s",
- abis, mTestArgs, mModuleArgs, mIncludeFiltersParsed, mExcludeFiltersParsed);
+ abis, mTestArgs, mModuleArgs, includeFilter, excludeFilter);
+
mModuleRepo =
createModuleLoader(
mIncludeFiltersParsed, mExcludeFiltersParsed, mTestArgs, mModuleArgs);
mModuleRepo.setParameterizedModules(mEnableParameter);
+ mModuleRepo.setOptionalParameterizedModules(mEnableOptionalParameter);
mModuleRepo.setModuleParameter(mForceParameter);
mModuleRepo.setExcludedModuleParameters(mExcludedModuleParameters);
@@ -250,15 +310,6 @@
return loadedConfigs;
}
- public File getTestsDir() throws FileNotFoundException {
- IBuildInfo build = getBuildInfo();
- if (build instanceof IDeviceBuildInfo) {
- return ((IDeviceBuildInfo) build).getTestsDir();
- }
- // TODO: handle multi build?
- throw new FileNotFoundException("Could not found a tests dir folder.");
- }
-
/** {@inheritDoc} */
@Override
public void setBuild(IBuildInfo buildInfo) {
@@ -405,4 +456,14 @@
protected void setPrioritizeHostConfig(boolean prioritizeHostConfig) {
mPrioritizeHostConfig = prioritizeHostConfig;
}
+
+ /** Log a file directly to the result reporter. */
+ private void logFilterFile(File filterFile, String dataName, LogDataType type) {
+ if (getCurrentTestLogger() == null) {
+ return;
+ }
+ try (FileInputStreamSource source = new FileInputStreamSource(filterFile)) {
+ getCurrentTestLogger().testLog(dataName, type, source);
+ }
+ }
}
diff --git a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
index 4b243ed..172ac97 100644
--- a/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
+++ b/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapper.java
@@ -29,25 +29,23 @@
import com.android.tradefed.result.ILogSaver;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.LogSaverResultForwarder;
-import com.android.tradefed.result.MergeStrategy;
-import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.ITestCollector;
import com.android.tradefed.testtype.ITestFilterReceiver;
-import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
+import com.android.tradefed.testtype.retry.BaseRetryDecision;
+import com.android.tradefed.testtype.retry.IRetryDecision;
+import com.android.tradefed.testtype.retry.MergeStrategy;
+import com.android.tradefed.testtype.retry.RetryStatistics;
+import com.android.tradefed.testtype.retry.RetryStrategy;
+import com.android.tradefed.util.StreamUtil;
import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Sets;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
* A wrapper class works on the {@link IRemoteTest} to granulate the IRemoteTest in testcase level.
@@ -87,16 +85,9 @@
private boolean mCollectTestsOnly = false;
// Tracking of the metrics
- /** How much time are we spending doing the retry attempts */
- private long mRetryTime = 0L;
- /** The number of test cases that passed after a failed attempt */
- private long mSuccessRetried = 0L;
- /** The number of test cases that remained failed after all retry attempts */
- private long mFailedRetried = 0L;
- /** Store the test that successfully re-run and at which attempt they passed */
- private Map<String, Integer> mAttemptSuccess = new HashMap<>();
+ private RetryStatistics mRetryStats = null;
- private RetryStrategy mRetryStrategy = RetryStrategy.RETRY_TEST_CASE_FAILURE;
+ private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
private boolean mRebootAtLastRetry = false;
public GranularRetriableTestWrapper(
@@ -204,7 +195,9 @@
}
// The module collectors itself are added: this list will be very limited.
- for (IMetricCollector collector : mModuleConfiguration.getMetricCollectors()) {
+ // We clone them since the configuration object is shared across shards.
+ for (IMetricCollector collector :
+ CollectorHelper.cloneCollectors(mModuleConfiguration.getMetricCollectors())) {
if (collector.isDisabled()) {
CLog.d("%s has been disabled. Skipping.", collector);
} else {
@@ -232,74 +225,23 @@
return;
}
- // If the very first attempt failed, then don't proceed.
- if (RetryStrategy.RERUN_UNTIL_FAILURE.equals(mRetryStrategy)) {
- Set<TestDescription> lastRun = getFailedTestCases(0);
- // If we encountered a failure
- if (!lastRun.isEmpty() || mMainGranularRunListener.hasRunCrashedAtAttempt(0)) {
- CLog.w("%s failed after the first run. Stopping.", lastRun);
- return;
- }
+ // Bail out early if there is no need to retry at all.
+ IRetryDecision retryDecision = new BaseRetryDecision(mRetryStrategy);
+ if (!retryDecision.shouldRetry(mTest, mMainGranularRunListener.getTestRunForAttempts(0))) {
+ return;
}
// Deal with retried attempted
long startTime = System.currentTimeMillis();
- Set<TestDescription> previousFailedTests = null;
- Set<String> originalFilters = new HashSet<>();
-
- // TODO(b/77548917): Right now we only support ITestFilterReceiver. We should expect to
- // support ITestFile*Filter*Receiver in the future.
- if (mTest instanceof ITestFilterReceiver) {
- ITestFilterReceiver test = (ITestFilterReceiver) mTest;
- originalFilters = new LinkedHashSet<>(test.getIncludeFilters());
- } else if (!shouldHandleFailure(mRetryStrategy)) {
- // TODO: improve this for test run failures, since they rerun the full run we should
- // be able to rerun even non-ITestFilterReceiver
- CLog.d("RetryStrategy does not involved moving filters proceeding with retry.");
- } else {
- CLog.d(
- "%s does not implement ITestFilterReceiver, thus cannot work with "
- + "intra-module retry.",
- mTest);
- return;
- }
-
try {
CLog.d("Starting intra-module retry.");
for (int attemptNumber = 1; attemptNumber < mMaxRunLimit; attemptNumber++) {
- CLog.d("Retry attempt number %s", attemptNumber);
- // Reset the filters to original.
- if (mTest instanceof ITestFilterReceiver) {
- ((ITestFilterReceiver) mTest).clearIncludeFilters();
- ((ITestFilterReceiver) mTest).addAllIncludeFilters(originalFilters);
- }
- // TODO: sort out the collection of metrics for each strategy
- if (shouldHandleFailure(mRetryStrategy)) {
- boolean shouldContinue = false;
- // In case of test run failure and we should retry test runs
- if (RetryStrategy.RETRY_TEST_RUN_FAILURE.equals(mRetryStrategy)
- || RetryStrategy.RETRY_ANY_FAILURE.equals(mRetryStrategy)) {
- if (mMainGranularRunListener.hasRunCrashedAtAttempt(attemptNumber - 1)) {
- CLog.d("Retrying the run failure.");
- shouldContinue = true;
- }
- }
-
- if (RetryStrategy.RETRY_TEST_CASE_FAILURE.equals(mRetryStrategy)
- || RetryStrategy.RETRY_ANY_FAILURE.equals(mRetryStrategy)) {
- // In case of test case failure, we retry with filters.
- previousFailedTests = getFailedTestCases(attemptNumber - 1);
- if (previousFailedTests.size() > 0 && !shouldContinue) {
- CLog.d("Retrying the test case failure.");
- shouldContinue = true;
- addRetriedTestsToIncludeFilters(mTest, previousFailedTests);
- }
- }
-
- if (!shouldContinue) {
- CLog.d("No test run or test case failures. No need to retry.");
- break;
- }
+ boolean retry =
+ retryDecision.shouldRetry(
+ mTest,
+ mMainGranularRunListener.getTestRunForAttempts(attemptNumber - 1));
+ if (!retry) {
+ return;
}
// Reboot device at the last intra-module retry if reboot-at-last-retry is set.
if (mRebootAtLastRetry && (attemptNumber == (mMaxRunLimit-1))) {
@@ -313,80 +255,14 @@
}
// Run the tests again
intraModuleRun(allListeners);
-
- Set<TestDescription> lastRun = getFailedTestCases(attemptNumber);
- if (shouldHandleFailure(mRetryStrategy)) {
- // Evaluate success from what we just ran
- if (previousFailedTests != null) {
- Set<TestDescription> diff = Sets.difference(previousFailedTests, lastRun);
- mSuccessRetried += diff.size();
- final int currentAttempt = attemptNumber;
- diff.forEach(
- (desc) -> mAttemptSuccess.put(desc.toString(), currentAttempt));
- previousFailedTests = lastRun;
- }
- }
-
- if (RetryStrategy.RERUN_UNTIL_FAILURE.equals(mRetryStrategy)) {
- // If we encountered a failure do not proceed
- if (!lastRun.isEmpty()
- || mMainGranularRunListener.hasRunCrashedAtAttempt(attemptNumber)) {
- CLog.w("%s failed at iteration %s. Stopping.", lastRun, attemptNumber);
- break;
- }
- }
}
+ // Feed the last attempt if we reached here.
+ retryDecision.addLastAttempt(
+ mMainGranularRunListener.getTestRunForAttempts(mMaxRunLimit - 1));
} finally {
- if (previousFailedTests != null) {
- mFailedRetried += previousFailedTests.size();
- }
+ mRetryStats = retryDecision.getRetryStats();
// Track how long we spend in retry
- mRetryTime = System.currentTimeMillis() - startTime;
- }
- }
-
- /**
- * If the strategy needs to handle some failures return True. If it needs to retry no matter
- * what like {@link RetryStrategy#ITERATIONS} returns False.
- */
- private boolean shouldHandleFailure(RetryStrategy retryStrategy) {
- return RetryStrategy.RETRY_ANY_FAILURE.equals(retryStrategy)
- || RetryStrategy.RETRY_TEST_RUN_FAILURE.equals(retryStrategy)
- || RetryStrategy.RETRY_TEST_CASE_FAILURE.equals(retryStrategy);
- }
-
- /**
- * Collect failed test cases from listener.
- *
- * @param attemptNumber the 0-indexed integer indicating which attempt to gather failed cases.
- */
- private Set<TestDescription> getFailedTestCases(int attemptNumber) {
- Set<TestDescription> failedTestCases = new HashSet<TestDescription>();
- for (String runName : mMainGranularRunListener.getTestRunNames()) {
- TestRunResult run =
- mMainGranularRunListener.getTestRunAtAttempt(runName, attemptNumber);
- if (run != null) {
- failedTestCases.addAll(run.getFailedTests());
- }
- }
- return failedTestCases;
- }
-
- /**
- * Update the arguments of {@link IRemoteTest} to only run failed tests. This arguments/logic is
- * implemented differently for each IRemoteTest testtype in the overridden
- * ITestFilterReceiver.addIncludeFilter method.
- *
- * @param test The {@link IRemoteTest} to evaluate as ITestFilterReceiver.
- * @param testDescriptions The set of failed testDescriptions to retry.
- */
- private void addRetriedTestsToIncludeFilters(
- IRemoteTest test, Set<TestDescription> testDescriptions) {
- if (test instanceof ITestFilterReceiver) {
- for (TestDescription testCase : testDescriptions) {
- String filter = testCase.toString();
- ((ITestFilterReceiver) test).addIncludeFilter(filter);
- }
+ mRetryStats.mRetryTime = System.currentTimeMillis() - startTime;
}
}
@@ -416,7 +292,7 @@
CLog.e("Module '%s' - test '%s' threw exception:", mModuleId, mTest.getClass());
CLog.e(re);
CLog.e("Proceeding to the next test.");
- runListener.testRunFailed(re.getMessage());
+ runListener.testRunFailed(StreamUtil.getStackTrace(re));
} catch (DeviceUnresponsiveException due) {
// being able to catch a DeviceUnresponsiveException here implies that recovery was
// successful, and test execution should proceed to next module.
@@ -439,26 +315,7 @@
/** Get the merged TestRunResults from each {@link IRemoteTest} run. */
public final List<TestRunResult> getFinalTestRunResults() {
- // TODO: Once we are ready to report break-down of results and option will override this.
- MergeStrategy strategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
- switch (mRetryStrategy) {
- case ITERATIONS:
- strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
- break;
- case RERUN_UNTIL_FAILURE:
- strategy = MergeStrategy.ANY_FAIL_IS_FAIL;
- break;
- case RETRY_ANY_FAILURE:
- strategy = MergeStrategy.ANY_PASS_IS_PASS;
- break;
- case RETRY_TEST_CASE_FAILURE:
- strategy = MergeStrategy.ONE_TESTCASE_PASS_IS_PASS;
- break;
- case RETRY_TEST_RUN_FAILURE:
- strategy = MergeStrategy.ONE_TESTRUN_PASS_IS_PASS;
- break;
- }
-
+ MergeStrategy strategy = MergeStrategy.getMergeStrategy(mRetryStrategy);
mMainGranularRunListener.setMergeStrategy(strategy);
return mMainGranularRunListener.getMergedTestRunResults();
}
@@ -477,11 +334,6 @@
return CollectorHelper.cloneCollectors(originalCollectors);
}
- /** Check if any testRunResult has ever failed. This check is used for bug report only. */
- public boolean hasFailed() {
- return mMainGranularRunListener.hasFailed();
- }
-
/**
* Calculate the number of testcases in the {@link IRemoteTest}. This value distincts the same
* testcases that are rescheduled multiple times.
@@ -490,19 +342,12 @@
return mMainGranularRunListener.getExpectedTests();
}
- /** Returns the elapsed time in retry attempts. */
- public final long getRetryTime() {
- return mRetryTime;
- }
-
- /** Returns the number of tests we managed to change status from failed to pass. */
- public final long getRetrySuccess() {
- return mSuccessRetried;
- }
-
- /** Returns the number of tests we couldn't change status from failed to pass. */
- public final long getRetryFailed() {
- return mFailedRetried;
+ /**
+ * Returns the {@link RetryStatistics} representating the retry information. Null if no retry
+ * occurred.
+ */
+ public final RetryStatistics getRetryStatistics() {
+ return mRetryStats;
}
/** Returns the listener containing all the results. */
@@ -510,11 +355,6 @@
return mMainGranularRunListener;
}
- /** Returns the attempts that turned into success. */
- public Map<String, Integer> getAttemptSuccessStats() {
- return mAttemptSuccess;
- }
-
/** Forwarder that also handles passing the current attempt we are at. */
private class RetryLogSaverResultForwarder extends LogSaverResultForwarder {
diff --git a/src/com/android/tradefed/testtype/suite/ITestSuite.java b/src/com/android/tradefed/testtype/suite/ITestSuite.java
index faa79be..370d6e5 100644
--- a/src/com/android/tradefed/testtype/suite/ITestSuite.java
+++ b/src/com/android/tradefed/testtype/suite/ITestSuite.java
@@ -18,7 +18,9 @@
import com.android.annotations.VisibleForTesting;
import com.android.ddmlib.Log.LogLevel;
import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.config.IDeviceConfiguration;
@@ -58,6 +60,7 @@
import com.android.tradefed.testtype.IRuntimeHintProvider;
import com.android.tradefed.testtype.IShardableTest;
import com.android.tradefed.testtype.ITestCollector;
+import com.android.tradefed.testtype.retry.RetryStrategy;
import com.android.tradefed.util.AbiFormatter;
import com.android.tradefed.util.AbiUtils;
import com.android.tradefed.util.MultiMap;
@@ -66,6 +69,8 @@
import com.google.inject.Inject;
import com.google.inject.Injector;
+import java.io.File;
+import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -80,6 +85,7 @@
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Abstract class used to run Test Suite. This class provide the base of how the Suite will be run.
@@ -98,30 +104,8 @@
IMetricCollectorReceiver,
IConfigurationReceiver,
IReportNotExecuted,
- ITokenRequest {
-
- /** The Retry Strategy to be used when re-running some tests. */
- public enum RetryStrategy {
- /** Rerun all the tests for the number of attempts specified. */
- ITERATIONS,
- /**
- * Rerun all the tests until the max count is reached or a failure occurs whichever come
- * first.
- */
- RERUN_UNTIL_FAILURE,
- /**
- * Rerun all the test case failures until passed or the max number of attempts specified.
- */
- RETRY_TEST_CASE_FAILURE,
- /** Rerun all the test run failures until passed or the max number of attempts specified. */
- RETRY_TEST_RUN_FAILURE,
- /**
- * Rerun all the test run and test cases failures until passed or the max number of attempts
- * specified. Test run failures are rerun in priority (a.k.a. if a run failure and a test
- * case failure occur, the run failure is rerun).
- */
- RETRY_ANY_FAILURE,
- }
+ ITokenRequest,
+ ITestLoggerReceiver {
public static final String SKIP_SYSTEM_STATUS_CHECKER = "skip-system-status-check";
public static final String RUNNER_WHITELIST = "runner-whitelist";
@@ -135,6 +119,8 @@
public static final String TOKEN_KEY = "token";
public static final String MODULE_METADATA_INCLUDE_FILTER = "module-metadata-include-filter";
public static final String MODULE_METADATA_EXCLUDE_FILTER = "module-metadata-exclude-filter";
+ public static final String RANDOM_SEED = "random-seed";
+ public static final String REBOOT_BEFORE_TEST = "reboot-before-test";
private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";
@@ -183,6 +169,12 @@
description = "Reboot the device at the last intra-module retry")
private boolean mRebootAtLastRetry = false;
+ @Option(
+ name = REBOOT_BEFORE_TEST,
+ description = "Reboot the device before the test suite starts."
+ )
+ private boolean mRebootBeforeTest = false;
+
@Option(name = "skip-all-system-status-check",
description = "Whether all system status check between modules should be skipped")
private boolean mSkipAllSystemStatusCheck = false;
@@ -211,7 +203,7 @@
private boolean mRandomOrder = false;
@Option(
- name = "random-seed",
+ name = RANDOM_SEED,
description = "Seed to randomize the order of the modules."
)
private long mRandomSeed = -1;
@@ -293,13 +285,8 @@
)
private boolean mIsolatedModule = false;
- @Option(
- name = "reboot-before-test",
- description = "Reboot the device before the test suite starts."
- )
- private boolean mRebootBeforeTest = false;
-
- // [Options relate to module retry and intra-module retry][
+ /** @deprecated to be deleted when next version is deployed */
+ @Deprecated
@Option(
name = "max-testcase-run-count",
description =
@@ -308,14 +295,17 @@
)
private int mMaxRunLimit = 1;
+ /** @deprecated to be deleted when next version is deployed */
+ @Deprecated
@Option(
name = "retry-strategy",
description =
"The retry strategy to be used when re-running some tests with "
+ "--max-testcase-run-count"
)
- private RetryStrategy mRetryStrategy = RetryStrategy.RETRY_TEST_CASE_FAILURE;
+ private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
+ // [Options relate to module retry and intra-module retry][
@Option(
name = "merge-attempts",
description = "Whether or not to use the merge the results of the different attempts."
@@ -341,6 +331,17 @@
// Current modules to run, null if not started to run yet.
private List<ModuleDefinition> mRunModules = null;
+ // Logger to be used to files.
+ private ITestLogger mCurrentLogger = null;
+ // Whether or not we are currently in split
+ private boolean mIsSplitting = false;
+
+ private DynamicRemoteFileResolver mDynamicResolver = new DynamicRemoteFileResolver();
+
+ @VisibleForTesting
+ void setDynamicResolver(DynamicRemoteFileResolver resolver) {
+ mDynamicResolver = resolver;
+ }
/**
* Get the current Guice {@link Injector} from the invocation. It should allow us to continue
@@ -382,6 +383,15 @@
}
}
+ public File getTestsDir() throws FileNotFoundException {
+ IBuildInfo build = getBuildInfo();
+ if (build instanceof IDeviceBuildInfo) {
+ return ((IDeviceBuildInfo) build).getTestsDir();
+ }
+ // TODO: handle multi build?
+ throw new FileNotFoundException("Could not found a tests dir folder.");
+ }
+
private LinkedHashMap<String, IConfiguration> loadAndFilter() {
LinkedHashMap<String, IConfiguration> runConfig = loadTests();
if (runConfig.isEmpty()) {
@@ -391,6 +401,7 @@
// Apply our guice scope to all modules objects
applyGuiceInjection(runConfig);
+ Set<String> moduleNames = new HashSet<>();
LinkedHashMap<String, IConfiguration> filteredConfig = new LinkedHashMap<>();
for (Entry<String, IConfiguration> config : runConfig.entrySet()) {
if (!mModuleMetadataIncludeFilter.isEmpty()
@@ -411,11 +422,51 @@
}
filterPreparers(config.getValue(), mAllowedPreparers);
filteredConfig.put(config.getKey(), config.getValue());
+ moduleNames.add(config.getValue().getConfigurationDescription().getModuleName());
}
+
+ if (mBuildInfo != null
+ && mBuildInfo.getRemoteFiles() != null
+ && mBuildInfo.getRemoteFiles().size() > 0) {
+ stageTestArtifacts(moduleNames);
+ }
+
runConfig.clear();
return filteredConfig;
}
+ /** Helper to download all artifacts for the given modules. */
+ private void stageTestArtifacts(Set<String> modules) {
+ CLog.i(String.format("Start to stage test artifacts for %d modules.", modules.size()));
+ long startTime = System.currentTimeMillis();
+ // Include the file if its path contains a folder name matching any of the module.
+ String moduleRegex =
+ modules.stream()
+ .map(m -> String.format("/%s/", m))
+ .collect(Collectors.joining("|"));
+ List<String> includeFilters = Arrays.asList(moduleRegex);
+ // Ignore config file as it's part of config zip artifact that's staged already.
+ List<String> excludeFilters = Arrays.asList("[.]config$");
+ for (File remoteFile : mBuildInfo.getRemoteFiles()) {
+ try {
+ mDynamicResolver.resolvePartialDownloadZip(
+ getTestsDir(), remoteFile.toString(), includeFilters, excludeFilters);
+ } catch (ConfigurationException | FileNotFoundException e) {
+ CLog.e(
+ String.format(
+ "Failed to download partial zip from %s for modules: %s",
+ remoteFile, String.join(", ", modules)));
+ CLog.e(e);
+ throw new RuntimeException(e);
+ }
+ }
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ CLog.i(
+ String.format(
+ "Staging test artifacts for %d modules finished in %s.",
+ modules.size(), TimeUtil.formatElapsedTime(elapsedTime)));
+ }
+
/** Helper that creates and returns the list of {@link ModuleDefinition} to be executed. */
private List<ModuleDefinition> createExecutionList() {
List<ModuleDefinition> runModules = new ArrayList<>();
@@ -478,6 +529,7 @@
}
CLog.i("Randomizing all the modules with seed: %s", randomSeed);
Collections.shuffle(runModules, new Random(randomSeed));
+ mBuildInfo.addBuildAttribute(RANDOM_SEED, String.valueOf(randomSeed));
}
private void checkClassLoad(Set<String> classes, String type) {
@@ -518,6 +570,7 @@
/** Generic run method for all test loaded from {@link #loadTests()}. */
@Override
public final void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+ mCurrentLogger = listener;
// Load and check the module checkers, runners and preparers in black and whitelist
checkClassLoad(mSystemStatusCheckBlacklist, SKIP_SYSTEM_STATUS_CHECKER);
checkClassLoad(mAllowedRunners, RUNNER_WHITELIST);
@@ -682,12 +735,17 @@
// Pass the main invocation logSaver
module.setLogSaver(mMainConfiguration.getLogSaver());
// Pass the retry strategy to the module
- module.setRetryStrategy(mRetryStrategy, mMergeAttempts);
+ module.setRetryStrategy(
+ getConfiguration().getCommandOptions().getRetryStrategy(), mMergeAttempts);
// Pass the reboot strategy at the last intra-module retry to the module
module.setRebootAtLastRetry(mRebootAtLastRetry);
// Actually run the module
- module.run(listener, moduleListeners, failureListener, mMaxRunLimit);
+ module.run(
+ listener,
+ moduleListeners,
+ failureListener,
+ getConfiguration().getCommandOptions().getMaxRetryCount());
if (!mSkipAllSystemStatusCheck) {
runPostModuleCheck(module.getId(), mSystemStatusCheckers, mDevice, listener);
@@ -813,6 +871,11 @@
System.currentTimeMillis() - startTime, new HashMap<String, Metric>());
}
+ /** Returns true if we are currently in {@link #split(int)}. */
+ public boolean isSplitting() {
+ return mIsSplitting;
+ }
+
/** {@inheritDoc} */
@Override
public Collection<IRemoteTest> split(int shardCountHint) {
@@ -820,39 +883,47 @@
// cannot shard or already sharded
return null;
}
+ mIsSplitting = true;
+ try {
+ LinkedHashMap<String, IConfiguration> runConfig = loadAndFilter();
+ if (runConfig.isEmpty()) {
+ CLog.i("No config were loaded. Nothing to run.");
+ return null;
+ }
+ injectInfo(runConfig);
- LinkedHashMap<String, IConfiguration> runConfig = loadAndFilter();
- if (runConfig.isEmpty()) {
- CLog.i("No config were loaded. Nothing to run.");
- return null;
+ // We split individual tests on double the shardCountHint to provide better average.
+ // The test pool mechanism prevent this from creating too much overhead.
+ List<ModuleDefinition> splitModules =
+ ModuleSplitter.splitConfiguration(
+ runConfig,
+ shardCountHint,
+ mShouldMakeDynamicModule,
+ mIntraModuleSharding);
+ runConfig.clear();
+ runConfig = null;
+
+ // Clean up the parent that will get sharded: It is fine to clean up before copying the
+ // options, because the sharded module is already created/populated so there is no need
+ // to carry these extra data.
+ cleanUpSuiteSetup();
+
+ // create an association of one ITestSuite <=> one ModuleDefinition as the smallest
+ // execution unit supported.
+ List<IRemoteTest> splitTests = new ArrayList<>();
+ for (ModuleDefinition m : splitModules) {
+ ITestSuite suite = createInstance();
+ OptionCopier.copyOptionsNoThrow(this, suite);
+ suite.mIsSharded = true;
+ suite.mDirectModule = m;
+ splitTests.add(suite);
+ }
+ // return the list of ITestSuite with their ModuleDefinition assigned
+ return splitTests;
+ } finally {
+ // Done splitting at that point
+ mIsSplitting = false;
}
- injectInfo(runConfig);
-
- // We split individual tests on double the shardCountHint to provide better average.
- // The test pool mechanism prevent this from creating too much overhead.
- List<ModuleDefinition> splitModules =
- ModuleSplitter.splitConfiguration(
- runConfig, shardCountHint, mShouldMakeDynamicModule, mIntraModuleSharding);
- runConfig.clear();
- runConfig = null;
-
- // Clean up the parent that will get sharded: It is fine to clean up before copying the
- // options, because the sharded module is already created/populated so there is no need
- // to carry these extra data.
- cleanUpSuiteSetup();
-
- // create an association of one ITestSuite <=> one ModuleDefinition as the smallest
- // execution unit supported.
- List<IRemoteTest> splitTests = new ArrayList<>();
- for (ModuleDefinition m : splitModules) {
- ITestSuite suite = createInstance();
- OptionCopier.copyOptionsNoThrow(this, suite);
- suite.mIsSharded = true;
- suite.mDirectModule = m;
- splitTests.add(suite);
- }
- // return the list of ITestSuite with their ModuleDefinition assigned
- return splitTests;
}
/**
@@ -963,9 +1034,14 @@
mContext = invocationContext;
}
- /** Set the max number of run attempt for each module. */
- public final void setMaxRunLimit(int maxRunLimit) {
- mMaxRunLimit = maxRunLimit;
+ /** {@inheritDoc} */
+ @Override
+ public void setTestLogger(ITestLogger testLogger) {
+ mCurrentLogger = testLogger;
+ }
+
+ public ITestLogger getCurrentTestLogger() {
+ return mCurrentLogger;
}
/** {@inheritDoc} */
@@ -987,6 +1063,11 @@
mMainConfiguration = configuration;
}
+ /** Returns the invocation {@link IConfiguration}. */
+ public final IConfiguration getConfiguration() {
+ return mMainConfiguration;
+ }
+
/** {@inheritDoc} */
@Override
public void reportNotExecuted(ITestInvocationListener listener) {
@@ -1154,6 +1235,11 @@
return mInjector;
}
+ /** Sets reboot-before-test to true. */
+ public final void enableRebootBeforeTest() {
+ mRebootBeforeTest = true;
+ }
+
/**
* Apply the metadata filter to the config and see if the config should run.
*
diff --git a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
index 45b78b0..665ec6f 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleDefinition.java
@@ -56,7 +56,8 @@
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.IRuntimeHintProvider;
import com.android.tradefed.testtype.ITestCollector;
-import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
+import com.android.tradefed.testtype.retry.RetryStatistics;
+import com.android.tradefed.testtype.retry.RetryStrategy;
import com.android.tradefed.testtype.suite.module.BaseModuleController;
import com.android.tradefed.testtype.suite.module.IModuleController.RunStrategy;
import com.android.tradefed.util.StreamUtil;
@@ -106,8 +107,6 @@
public static final String RETRY_SUCCESS_COUNT = "MODULE_RETRY_SUCCESS";
public static final String RETRY_FAIL_COUNT = "MODULE_RETRY_FAILED";
- private static final String FLAKE_DATE_PREFIX = "FLAKE_DATA:";
-
private final IInvocationContext mModuleInvocationContext;
private final IConfiguration mModuleConfiguration;
private ILogSaver mLogSaver;
@@ -135,13 +134,9 @@
private long mStartTestTime = 0l;
// Tracking of retry performance
- private long mRetryTime = 0L;
- /** The number of test cases that passed after a failed attempt */
- private long mSuccessRetried = 0L;
- /** The number of test cases that remained failed after all retry attempts */
- private long mFailedRetried = 0L;
+ private List<RetryStatistics> mRetryStats = new ArrayList<>();
- private RetryStrategy mRetryStrategy = RetryStrategy.RETRY_TEST_CASE_FAILURE;
+ private RetryStrategy mRetryStrategy = RetryStrategy.NO_RETRY;
private boolean mMergeAttempts = true;
private boolean mRebootAtLastRetry = false;
@@ -491,15 +486,14 @@
mExpectedTests += retriableTest.getExpectedTestsCount();
// Get information about retry
- mRetryTime += retriableTest.getRetryTime();
- mSuccessRetried += retriableTest.getRetrySuccess();
- mFailedRetried += retriableTest.getRetryFailed();
-
- addAttemptStatsToBuild(mBuild, retriableTest.getAttemptSuccessStats());
+ RetryStatistics res = retriableTest.getRetryStatistics();
+ if (res != null) {
+ mRetryStats.add(res);
+ }
}
// After the run, if the test failed (even after retry the final result passed) has
// failed, capture a bugreport.
- if (retriableTest.hasFailed()) {
+ if (retriableTest.getResultListener().hasFailed()) {
captureBugreport(listener, getId());
}
}
@@ -655,13 +649,16 @@
metricsProto.put(
TEST_TIME, TfMetricProtoUtil.createSingleValue(elapsedTime, "milliseconds"));
// Report all the retry informations
- if (mRetryTime > 0L) {
+ if (!mRetryStats.isEmpty()) {
+ RetryStatistics agg = RetryStatistics.aggregateStatistics(mRetryStats);
metricsProto.put(
- RETRY_TIME, TfMetricProtoUtil.createSingleValue(mRetryTime, "milliseconds"));
+ RETRY_TIME,
+ TfMetricProtoUtil.createSingleValue(agg.mRetryTime, "milliseconds"));
metricsProto.put(
- RETRY_SUCCESS_COUNT, TfMetricProtoUtil.createSingleValue(mSuccessRetried, ""));
+ RETRY_SUCCESS_COUNT,
+ TfMetricProtoUtil.createSingleValue(agg.mRetrySuccess, ""));
metricsProto.put(
- RETRY_FAIL_COUNT, TfMetricProtoUtil.createSingleValue(mFailedRetried, ""));
+ RETRY_FAIL_COUNT, TfMetricProtoUtil.createSingleValue(agg.mRetryFailure, ""));
}
if (totalExpectedTests != numResults) {
@@ -968,11 +965,4 @@
}
return RunStrategy.RUN;
}
-
- private void addAttemptStatsToBuild(IBuildInfo build, Map<String, Integer> attemptStats) {
- for (Entry<String, Integer> entry : attemptStats.entrySet()) {
- String key = String.format("%s%s:%s", FLAKE_DATE_PREFIX, getId(), entry.getKey());
- build.addBuildAttribute(key, Integer.toString(entry.getValue()));
- }
- }
}
diff --git a/src/com/android/tradefed/testtype/suite/ModuleListener.java b/src/com/android/tradefed/testtype/suite/ModuleListener.java
index 5c78229..86057d2 100644
--- a/src/com/android/tradefed/testtype/suite/ModuleListener.java
+++ b/src/com/android/tradefed/testtype/suite/ModuleListener.java
@@ -26,7 +26,6 @@
import com.android.tradefed.result.LogFile;
import com.android.tradefed.result.LogSaverResultForwarder;
import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.testtype.IRemoteTest;
import java.util.HashMap;
@@ -61,6 +60,11 @@
@Override
public void testRunStarted(String name, int numTests, int attemptNumber) {
+ testRunStarted(name, numTests, attemptNumber, System.currentTimeMillis());
+ }
+
+ @Override
+ public void testRunStarted(String name, int numTests, int attemptNumber, long startTime) {
mRunInProgress = true;
// In case of retry of the same run, do not add the expected count again. This allows
// situation where test runner has a built-in retry (like InstrumentationTest) and calls
@@ -68,10 +72,11 @@
if (getTestRunAtAttempt(name, attemptNumber) != null) {
numTests = 0;
}
- super.testRunStarted(name, numTests, attemptNumber);
+ super.testRunStarted(name, numTests, attemptNumber, startTime);
if (attemptNumber != 0) {
mTestsRan = 1;
}
+ CLog.d("ModuleListener.testRunStarted(%s, %s, %s)", name, numTests, attemptNumber);
}
/** {@inheritDoc} */
@@ -111,7 +116,12 @@
private void logTestPassed(String testName) {
if (!mTestFailed && !mCollectTestsOnly) {
CLog.logAndDisplay(
- LogLevel.INFO, "[%d/%d] %s pass", mTestsRan, getExpectedTests(), testName);
+ LogLevel.INFO,
+ "[%d/%d] %s %s pass",
+ mTestsRan,
+ getExpectedTests(),
+ getCurrentRunResults().getName(),
+ testName);
}
mTestsRan++;
}
@@ -137,9 +147,10 @@
}
CLog.logAndDisplay(
LogLevel.INFO,
- "[%d/%d] %s fail:\n%s",
+ "[%d/%d] %s %s fail:\n%s",
mTestsRan,
getExpectedTests(),
+ getCurrentRunResults().getName(),
test.toString(),
trace);
mTestFailed = true;
@@ -188,20 +199,4 @@
}
}
}
-
- /**
- * Check if any runs in the given attempt have incompleted (aka "run failure").
- *
- * @param attemptNumber indicates which attempt should the test runs come from.
- * @return true if any of the runs in the given attempt has crashed.
- */
- public boolean hasRunCrashedAtAttempt(int attemptNumber) {
- for (String runName : getTestRunNames()) {
- TestRunResult run = getTestRunAtAttempt(runName, attemptNumber);
- if (run != null && run.isRunFailure()) {
- return true;
- }
- }
- return false;
- }
}
diff --git a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
index 78d7b2f..08dd626 100644
--- a/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
+++ b/src/com/android/tradefed/testtype/suite/SuiteModuleLoader.java
@@ -15,12 +15,13 @@
*/
package com.android.tradefed.testtype.suite;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
+import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
import com.android.tradefed.config.ConfigurationUtil;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationFactory;
+import com.android.tradefed.config.OptionDef;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.targetprep.ITargetPreparer;
import com.android.tradefed.testtype.IAbi;
@@ -71,6 +72,7 @@
private IConfigurationFactory mConfigFactory = ConfigurationFactory.getInstance();
private boolean mAllowParameterizedModules = false;
+ private boolean mAllowOptionalParameterizedModules = false;
private ModuleParameters mForcedModuleParameter = null;
private Set<ModuleParameters> mExcludedModuleParameters = new HashSet<>();
@@ -100,6 +102,11 @@
mAllowParameterizedModules = allowed;
}
+ /** Sets whether or not to allow optional parameterized modules. */
+ public final void setOptionalParameterizedModules(boolean allowed) {
+ mAllowOptionalParameterizedModules = allowed;
+ }
+
/** Sets the only {@link ModuleParameters} type that should be run. */
public final void setModuleParameter(ModuleParameters param) {
mForcedModuleParameter = param;
@@ -156,17 +163,16 @@
*
* @param test The {@link IRemoteTest} that is being considered.
* @param abi The Abi we are currently working on.
- * @param name The name of the module.
+ * @param moduleId The id of the module (usually abi + module name).
* @param includeFilters The formatted and parsed include filters.
* @param excludeFilters The formatted and parsed exclude filters.
*/
public void addFiltersToTest(
IRemoteTest test,
IAbi abi,
- String name,
+ String moduleId,
Map<String, List<SuiteTestFilter>> includeFilters,
Map<String, List<SuiteTestFilter>> excludeFilters) {
- String moduleId = AbiUtils.createId(abi.getName(), name);
if (!(test instanceof ITestFilterReceiver)) {
CLog.e("Test in module %s does not implement ITestFilterReceiver.", moduleId);
return;
@@ -174,10 +180,10 @@
List<SuiteTestFilter> mdIncludes = getFilterList(includeFilters, moduleId);
List<SuiteTestFilter> mdExcludes = getFilterList(excludeFilters, moduleId);
if (!mdIncludes.isEmpty()) {
- addTestIncludes((ITestFilterReceiver) test, mdIncludes, name);
+ addTestIncludes((ITestFilterReceiver) test, mdIncludes, moduleId);
}
if (!mdExcludes.isEmpty()) {
- addTestExcludes((ITestFilterReceiver) test, mdExcludes, name);
+ addTestExcludes((ITestFilterReceiver) test, mdExcludes, moduleId);
}
}
@@ -204,7 +210,8 @@
if (mForcedModuleParameter != null) {
mForcedParameter =
ModuleParametersHelper.getParameterHandler(
- mForcedModuleParameter, /* optionalParams */ false);
+ mForcedModuleParameter, /* optionalParams */
+ mAllowOptionalParameterizedModules);
}
// Invokes parser to process the test module config file
@@ -296,6 +303,12 @@
if (shouldRunParameterized(baseId, fullId)) {
IConfiguration paramConfig =
mConfigFactory.createConfigurationFromArgs(pathArg);
+ // Mark the parameter in the metadata
+ paramConfig
+ .getConfigurationDescription()
+ .addMetadata(
+ ConfigurationDescriptor.PARAMETER_KEY,
+ param.getParameterIdentifier());
setUpConfig(name, baseId, fullId, paramConfig, abi);
param.applySetup(paramConfig);
toRun.put(fullId, paramConfig);
@@ -402,9 +415,11 @@
}
private void addTestIncludes(
- ITestFilterReceiver test, List<SuiteTestFilter> includes, String name) {
+ ITestFilterReceiver test, List<SuiteTestFilter> includes, String moduleId) {
if (test instanceof ITestFileFilterReceiver) {
- File includeFile = createFilterFile(name, ".include", includes);
+ // module id can contain spaces, avoid them for file names.
+ String escapedFileName = moduleId.replaceAll(" ", "_");
+ File includeFile = createFilterFile(escapedFileName, ".include", includes);
((ITestFileFilterReceiver) test).setIncludeTestFile(includeFile);
} else {
// add test includes one at a time
@@ -507,7 +522,7 @@
String optionValueString = remainder.substring(optionNameSep + 1);
// TODO: See if QuotationTokenizer can be improved for multi-character delimiter.
// or change the delimiter to a single char.
- String[] tokens = optionValueString.split(":=");
+ String[] tokens = optionValueString.split(":=", 2);
OptionDef option = null;
if (tokens.length == 1) {
option = new OptionDef(optionName, tokens[0], moduleName);
@@ -545,13 +560,15 @@
}
// Do not consider the excluded parameterization dimension
if (mExcludedModuleParameters.contains(suiteParam)) {
- CLog.d("'%s' was excluded via exclude-module-parameters.");
+ CLog.d("'%s' was excluded via exclude-module-parameters.", moduleName);
continue;
}
IModuleParameter handler =
ModuleParametersHelper.getParameterHandler(
- suiteParam, /* optionalParams */ false);
- params.add(handler);
+ suiteParam, /* optionalParams */ mAllowOptionalParameterizedModules);
+ if (handler != null) {
+ params.add(handler);
+ }
}
return params;
}
@@ -561,7 +578,7 @@
*
* @param name The base name of the module
* @param id The base id name of the module.
- * @param fullId The full id of the module.
+ * @param fullId The full id of the module (usually abi + module name + parameters)
* @param config The module configuration.
* @param abi The abi of the module.
* @throws ConfigurationException
@@ -595,7 +612,7 @@
if (mTestOptions.containsKey(className)) {
config.injectOptionValues(mTestOptions.get(className));
}
- addFiltersToTest(test, abi, name, mIncludeFilters, mExcludeFilters);
+ addFiltersToTest(test, abi, fullId, mIncludeFilters, mExcludeFilters);
if (test instanceof IAbiReceiver) {
((IAbiReceiver) test).setAbi(abi);
}
@@ -605,7 +622,7 @@
config.getConfigurationDescription().setAbi(abi);
config.getConfigurationDescription().setModuleName(name);
- config.validateOptions(false);
+ config.validateOptions();
}
/** Whether or not the base configuration should be created for all abis or not. */
diff --git a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
index c0dc16a..b13ffa7 100644
--- a/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
+++ b/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunner.java
@@ -18,6 +18,7 @@
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.testmapping.TestInfo;
import com.android.tradefed.util.testmapping.TestMapping;
import com.android.tradefed.util.testmapping.TestOption;
@@ -29,6 +30,8 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
+
/**
* Implementation of {@link BaseTestSuite} to run tests specified by option include-filter, or
* TEST_MAPPING files from build, as a suite.
@@ -56,6 +59,14 @@
)
private Set<String> mKeywords = new HashSet<>();
+ @Option(
+ name = "force-test-mapping-module",
+ description =
+ "Run the specified tests only. The tests loaded from all TEST_MAPPING files in "
+ + "the source code will be filtered again to force run the specified tests."
+ )
+ private Set<String> mTestModulesForced = new HashSet<>();
+
/** Special definition in the test mapping structure. */
private static final String TEST_MAPPING_INCLUDE_FILTER = "include-filter";
@@ -91,6 +102,11 @@
throw new RuntimeException(
"Must specify --test-mapping-test-group when applying --test-mapping-keyword.");
}
+ if (mTestGroup == null && !mTestModulesForced.isEmpty()) {
+ throw new RuntimeException(
+ "Must specify --test-mapping-test-group when applying "
+ + "--force-test-mapping-module.");
+ }
if (mTestGroup != null && !includeFilter.isEmpty()) {
throw new RuntimeException(
"If options --test-mapping-test-group is set, option --include-filter should "
@@ -101,6 +117,14 @@
Set<TestInfo> testsToRun =
TestMapping.getTests(
getBuildInfo(), mTestGroup, getPrioritizeHostConfig(), mKeywords);
+ if (!mTestModulesForced.isEmpty()) {
+ CLog.i("Filtering tests for the given names: %s", mTestModulesForced);
+ testsToRun =
+ testsToRun
+ .stream()
+ .filter(testInfo -> mTestModulesForced.contains(testInfo.getName()))
+ .collect(Collectors.toSet());
+ }
if (testsToRun.isEmpty()) {
throw new RuntimeException(
String.format("No test found for the given group: %s.", mTestGroup));
diff --git a/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java b/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
index 8fca54d..ad77eee 100644
--- a/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
+++ b/src/com/android/tradefed/testtype/suite/params/ModuleParameters.java
@@ -24,7 +24,8 @@
MULTI_ABI("multi_abi", "multi_abi_family"),
NOT_MULTI_ABI("not_multi_abi", "multi_abi_family"),
- SECONDARY_USER("secondary_user", "secondary_user_family");
+ SECONDARY_USER("secondary_user", "secondary_user_family"),
+ NOT_SECONDARY_USER("not_secondary_user", "secondary_user_family");
public static final String INSTANT_APP_FAMILY = "instant_app_family";
public static final String MULTI_ABI_FAMILY = "multi_abi_family";
diff --git a/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java b/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
index e009976..9a378e4 100644
--- a/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
+++ b/src/com/android/tradefed/testtype/suite/params/ModuleParametersHelper.java
@@ -41,6 +41,7 @@
static {
sOptionalHandlerMap.put(ModuleParameters.SECONDARY_USER, new SecondaryUserHandler());
+ sOptionalHandlerMap.put(ModuleParameters.NOT_SECONDARY_USER, new NegativeHandler());
}
/**
diff --git a/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java b/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
index f4a133d..cdaaca4 100644
--- a/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
+++ b/src/com/android/tradefed/testtype/suite/params/SecondaryUserHandler.java
@@ -19,6 +19,7 @@
import com.android.tradefed.config.IDeviceConfiguration;
import com.android.tradefed.targetprep.CreateUserPreparer;
import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.RunCommandTargetPreparer;
import java.util.List;
@@ -34,7 +35,17 @@
public void applySetup(IConfiguration moduleConfiguration) {
for (IDeviceConfiguration deviceConfig : moduleConfiguration.getDeviceConfig()) {
List<ITargetPreparer> preparers = deviceConfig.getTargetPreparers();
+ // The first things module will do is switch to a secondary user
preparers.add(0, new CreateUserPreparer());
+ // Add a preparer to setup the location settings on the new user
+ preparers.add(1, createLocationPreparer());
}
}
+
+ private RunCommandTargetPreparer createLocationPreparer() {
+ RunCommandTargetPreparer location = new RunCommandTargetPreparer();
+ location.addRunCommand("settings put secure location_providers_allowed +gps");
+ location.addRunCommand("settings put secure location_providers_allowed +network");
+ return location;
+ }
}
diff --git a/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java b/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
index 3b7f914..96538ec 100644
--- a/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
+++ b/src/com/android/tradefed/testtype/suite/retry/ResultsPlayer.java
@@ -15,6 +15,9 @@
*/
package com.android.tradefed.testtype.suite.retry;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.IConfigurationReceiver;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.StubDevice;
@@ -38,7 +41,8 @@
import java.util.Map.Entry;
/** Special runner that replays the results given to it. */
-public final class ResultsPlayer implements IRemoteTest, IInvocationContextReceiver {
+public final class ResultsPlayer
+ implements IRemoteTest, IInvocationContextReceiver, IConfigurationReceiver {
private class ReplayModuleHolder {
public IInvocationContext mModuleContext;
@@ -47,6 +51,7 @@
private IInvocationContext mContext;
private Map<TestRunResult, ReplayModuleHolder> mModuleResult;
+ private IConfiguration mConfiguration;
/** Ctor. */
public ResultsPlayer() {
@@ -66,7 +71,13 @@
}
long startReplay = System.currentTimeMillis();
- CLog.d("Start replaying the previous results.");
+ CLog.logAndDisplay(
+ LogLevel.DEBUG,
+ "Start replaying the previous results. Please wait this can take a few minutes.");
+ // Change the logging level to avoid too much logs from the replay.
+ LogLevel originalLevel = mConfiguration.getLogOutput().getLogLevel();
+ mConfiguration.getLogOutput().setLogLevel(LogLevel.WARN);
+
for (TestRunResult module : mModuleResult.keySet()) {
ReplayModuleHolder holder = mModuleResult.get(module);
@@ -96,7 +107,10 @@
// memory early
holder.mResults.clear();
}
- CLog.d(
+ // Restore the original log level to continue execution with the requested log level.
+ mConfiguration.getLogOutput().setLogLevel(originalLevel);
+ CLog.logAndDisplay(
+ LogLevel.DEBUG,
"Done replaying results in %s",
TimeUtil.formatElapsedTime(System.currentTimeMillis() - startReplay));
mModuleResult.clear();
@@ -131,6 +145,12 @@
mContext = invocationContext;
}
+ /** {@inheritDoc} */
+ @Override
+ public void setConfiguration(IConfiguration configuration) {
+ mConfiguration = configuration;
+ }
+
private void forwardTestResults(
TestRunResult module,
Collection<Entry<TestDescription, TestResult>> testSet,
diff --git a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
index 5358794..666bd44 100644
--- a/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
+++ b/src/com/android/tradefed/testtype/suite/retry/RetryRescheduler.java
@@ -37,7 +37,9 @@
import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.suite.BaseTestSuite;
+import com.android.tradefed.testtype.suite.ITestSuite;
import com.android.tradefed.testtype.suite.SuiteTestFilter;
+import com.android.tradefed.util.AbiUtils;
import com.android.tradefed.util.QuotationAwareTokenizer;
import com.android.tradefed.util.TestRecordInterpreter;
@@ -85,6 +87,13 @@
)
private Set<String> mExcludeFilters = new HashSet<>();
+ // Carry some options from suites that are convenient and don't impact the tests selection.
+ @Option(
+ name = ITestSuite.REBOOT_BEFORE_TEST,
+ description = "Reboot the device before the test suite starts."
+ )
+ private boolean mRebootBeforeTest = false;
+
public static final String PREVIOUS_LOADER_NAME = "previous_loader";
private IConfiguration mConfiguration;
@@ -168,6 +177,10 @@
// Do the customization of the configuration for specialized use cases.
customizeConfig(previousLoader, originalConfig);
+ if (mRebootBeforeTest) {
+ suite.enableRebootBeforeTest();
+ }
+
mRescheduledConfiguration = originalConfig;
if (mRescheduler != null) {
@@ -222,10 +235,28 @@
} else {
types.add(mRetryType);
}
+
+ // Expand the exclude-filter in case no abi is specified.
+ Set<String> extendedExcludeRetryFilters = new HashSet<>();
+ for (String excludeFilter : mExcludeFilters) {
+ SuiteTestFilter suiteFilter = SuiteTestFilter.createFrom(excludeFilter);
+ // Keep the current exclude-filter
+ extendedExcludeRetryFilters.add(excludeFilter);
+ if (suiteFilter.getAbi() == null) {
+ // If no abi is specified, exclude them all.
+ Set<String> abis = AbiUtils.getAbisSupportedByCompatibility();
+ for (String abi : abis) {
+ SuiteTestFilter namingFilter =
+ new SuiteTestFilter(abi, suiteFilter.getName(), suiteFilter.getTest());
+ extendedExcludeRetryFilters.add(namingFilter.toString());
+ }
+ }
+ }
+
// Prepare exclusion filters
for (TestRunResult moduleResult : results.getMergedTestRunResults()) {
// If the module is explicitly excluded from retries, preserve the original results.
- if (!mExcludeFilters.contains(moduleResult.getName())
+ if (!extendedExcludeRetryFilters.contains(moduleResult.getName())
&& RetryResultHelper.shouldRunModule(moduleResult, types)) {
if (types.contains(RetryType.NOT_EXECUTED)) {
// Clear the run failure since we are attempting to rerun all non-executed
diff --git a/src/com/android/tradefed/testtype/testdefs/InstrumentationTestDef.java b/src/com/android/tradefed/testtype/testdefs/InstrumentationTestDef.java
deleted file mode 100644
index 0a0b4b3..0000000
--- a/src/com/android/tradefed/testtype/testdefs/InstrumentationTestDef.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-/**
- * Container object for a single test def's data
- */
-class InstrumentationTestDef {
- private final String mName;
- private final String mPackage;
- private String mRunner = null;
- private String mClassName = null;
- private boolean mIsContinuous = false;
- private String mCoverageTarget = null;
-
- /**
- * Creates a {@link InstrumentationTestDef}.
- *
- * @param testName the unique test name
- * @param packageName the Android manifest package name of the test.
- */
- public InstrumentationTestDef(String testName, String packageName) {
- mName = testName;
- mPackage = packageName;
- }
-
- void setRunner(String runnerName) {
- mRunner = runnerName;
- }
-
- void setClassName(String className) {
- mClassName = className;
- }
-
- void setContinuous(boolean isContinuous) {
- mIsContinuous = isContinuous;
- }
-
- void setCoverageTarget(String coverageTarget) {
- mCoverageTarget = coverageTarget;
- }
-
- /**
- * Returns the unique name of the test definition.
- */
- String getName() {
- return mName;
- }
-
- /**
- * Returns the Android Manifest package name of the test application.
- */
- String getPackage() {
- return mPackage;
- }
-
- /**
- * Returns the fully specified name of the instrumentation runner to use. <code>null</code>
- * if not specified.
- */
- String getRunner() {
- return mRunner;
- }
-
- /**
- * Returns the fully specified name of the test class to run. <code>null</code> if not
- * specified.
- */
- String getClassName() {
- return mClassName;
- }
-
- /**
- * Returns <code>true</code> if test is part of the continuous test.
- */
- boolean isContinuous() {
- return mIsContinuous;
- }
-
- /**
- * Returns the coverage target for the test.
- */
- String getCoverageTarget() {
- return mCoverageTarget;
- }
-}
diff --git a/src/com/android/tradefed/testtype/testdefs/XmlDefsParser.java b/src/com/android/tradefed/testtype/testdefs/XmlDefsParser.java
deleted file mode 100644
index e489052..0000000
--- a/src/com/android/tradefed/testtype/testdefs/XmlDefsParser.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-import com.android.tradefed.util.xml.AbstractXmlParser;
-
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Parses a test_defs.xml file.
- * <p/>
- * See development/testrunner/test_defs.xsd for expected format
- */
-class XmlDefsParser extends AbstractXmlParser {
-
- private Map<String, InstrumentationTestDef> mTestDefsMap;
-
- /**
- * SAX callback object. Handles parsing data from the xml tags.
- */
- private class DefsHandler extends DefaultHandler {
-
- private static final String TEST_TAG = "test";
-
- @Override
- public void startElement(String uri, String localName, String name, Attributes attributes)
- throws SAXException {
- if (TEST_TAG.equals(localName)) {
- final String defName = attributes.getValue("name");
- InstrumentationTestDef def = new InstrumentationTestDef(defName,
- attributes.getValue("package"));
- def.setClassName(attributes.getValue("class"));
- def.setRunner(attributes.getValue("runner"));
- def.setContinuous("true".equals(attributes.getValue("continuous")));
- def.setCoverageTarget(attributes.getValue("coverage_target"));
- mTestDefsMap.put(defName, def);
- }
- }
- }
-
- XmlDefsParser() {
- // Uses a LinkedHashmap to have predictable iteration order
- mTestDefsMap = new LinkedHashMap<String, InstrumentationTestDef>();
- }
-
- /**
- * Gets the list of parsed test definitions. The element order should be consistent with the
- * order of elements in the parsed input.
- */
- public Collection<InstrumentationTestDef> getTestDefs() {
- return mTestDefsMap.values();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected DefaultHandler createXmlHandler() {
- return new DefsHandler();
- }
-}
diff --git a/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java b/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
deleted file mode 100644
index 03e408d..0000000
--- a/src/com/android/tradefed/testtype/testdefs/XmlDefsTest.java
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.Option.Importance;
-import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.IDeviceTest;
-import com.android.tradefed.testtype.IRemoteTest;
-import com.android.tradefed.testtype.IResumableTest;
-import com.android.tradefed.testtype.IShardableTest;
-import com.android.tradefed.testtype.InstrumentationTest;
-import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.proto.TfMetricProtoUtil;
-import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-
-/**
- * Runs a set of instrumentation test's defined in test_defs.xml files.
- * <p/>
- * The test definition files can either be one or more files on local file system, and/or one or
- * more files stored on the device under test.
- */
-@OptionClass(alias = "xml-defs")
-public class XmlDefsTest implements IDeviceTest, IResumableTest,
- IShardableTest {
-
- private static final String LOG_TAG = "XmlDefsTest";
-
- /** the metric key name for the test coverage target value */
- // TODO: move this to a more generic location
- public static final String COVERAGE_TARGET_KEY = "coverage_target";
-
- private ITestDevice mDevice;
-
- /**
- * @deprecated use shell-timeout or test-timeout instead.
- */
- @Deprecated
- @Option(name = "timeout",
- description="Deprecated - Use \"shell-timeout\" or \"test-timeout\" instead.")
- private Integer mTimeout = null;
-
- @Option(name = "shell-timeout",
- description="The defined timeout (in milliseconds) is used as a maximum waiting time "
- + "when expecting the command output from the device. At any time, if the "
- + "shell command does not output anything for a period longer than defined "
- + "timeout the TF run terminates. For no timeout, set to 0.")
- private long mShellTimeout = 10 * 60 * 1000; // default to 10 minutes
-
- @Option(name = "test-timeout",
- description="Sets timeout (in milliseconds) that will be applied to each test. In the "
- + "event of a test timeout it will log the results and proceed with executing "
- + "the next test. For no timeout, set to 0.")
- private int mTestTimeout = 10 * 60 * 1000; // default to 10 minutes
-
- @Option(name = "size",
- description = "Restrict tests to a specific test size. " +
- "One of 'small', 'medium', 'large'",
- importance = Importance.IF_UNSET)
- private String mTestSize = null;
-
- @Option(name = "rerun",
- description = "Rerun unexecuted tests individually on same device if test run " +
- "fails to complete.")
- private boolean mIsRerunMode = true;
-
- @Option(name = "resume",
- description = "Schedule unexecuted tests for resumption on another device " +
- "if first device becomes unavailable.")
- private boolean mIsResumeMode = false;
-
- @Option(name = "local-file-path",
- description = "local file path to test_defs.xml file to run.")
- private Collection<File> mLocalFiles = new ArrayList<File>();
-
- @Option(name = "device-file-path",
- description = "file path on device to test_defs.xml file to run.",
- importance = Importance.IF_UNSET)
- private Collection<String> mRemotePaths = new ArrayList<String>();
-
- @Option(name = "send-coverage",
- description = "Send coverage target info to test listeners.")
- private boolean mSendCoverage = true;
-
- @Option(name = "num-shards",
- description = "Shard this test into given number of separately runnable chunks.")
- private int mNumShards = 0;
-
- private List<InstrumentationTest> mTests = null;
-
- public XmlDefsTest() {
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public ITestDevice getDevice() {
- return mDevice;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void setDevice(ITestDevice device) {
- mDevice = device;
- }
-
- /**
- * Adds a remote test def file path.
- * <p/>
- * Exposed for unit testing.
- */
- void addRemoteFilePath(String path) {
- mRemotePaths.add(path);
- }
-
- /**
- * Adds a local test def file path.
- * <p/>
- * Exposed for unit testing.
- */
- void addLocalFilePath(File file) {
- mLocalFiles.add(file);
- }
-
- /**
- * Set the send coverage flag.
- * <p/>
- * Exposed for unit testing.
- */
- void setSendCoverage(boolean sendCoverage) {
- mSendCoverage = sendCoverage;
- }
-
- /**
- * Sets the number of shards test should be split into
- * <p/>
- * Exposed for unit testing.
- */
- void setNumShards(int shards) {
- mNumShards = shards;
- }
-
- /**
- * Gets the list of parsed {@link InstrumentationTest}s contained within.
- * <p/>
- * Exposed for unit testing.
- */
- List<InstrumentationTest> getTests() {
- return mTests;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
- if (getDevice() == null) {
- throw new IllegalArgumentException("Device has not been set");
- }
- buildTests();
- doRun(listener);
- }
-
- /**
- * Build the list of tests to run from the xml files, if not done already.
- * @throws DeviceNotAvailableException
- */
- private void buildTests() throws DeviceNotAvailableException {
- if (mTests == null) {
- if (mLocalFiles.isEmpty() && mRemotePaths.isEmpty()) {
- throw new IllegalArgumentException("No test definition files (local-file-path or " +
- "device-file-path) have been provided.");
- }
- XmlDefsParser parser = createParser();
- for (File testDefFile : mLocalFiles) {
- parseFile(parser, testDefFile);
- }
- for (File testDefFile : getRemoteFile(mRemotePaths)) {
- try {
- parseFile(parser, testDefFile);
- } finally {
- testDefFile.delete();
- }
- }
-
- mTests = new LinkedList<InstrumentationTest>();
- for (InstrumentationTestDef def : parser.getTestDefs()) {
- // only run continuous for now. Consider making this configurable
- if (def.isContinuous()) {
- InstrumentationTest test = createInstrumentationTest();
-
- test.setDevice(getDevice());
- test.setPackageName(def.getPackage());
- if (def.getRunner() != null) {
- test.setRunnerName(def.getRunner());
- }
- if (def.getClassName() != null) {
- test.setClassName(def.getClassName());
- }
- test.setRerunMode(mIsRerunMode);
- test.setResumeMode(mIsResumeMode);
- test.setTestSize(getTestSize());
- if (mTimeout != null) {
- LogUtil.CLog
- .w("\"timeout\" argument is deprecated and should not be used! \"shell-timeout\""
- + " argument value is overwritten with %d ms", mTimeout);
- setShellTimeout(mTimeout);
- }
- test.setShellTimeout(getShellTimeout());
- test.setTestTimeout(getTestTimeout());
- test.setCoverageTarget(def.getCoverageTarget());
- mTests.add(test);
- }
- }
- }
- }
-
- /**
- * Parse the given xml def file
- *
- * @param parser
- * @param testDefFile
- */
- private void parseFile(XmlDefsParser parser, File testDefFile) {
- try {
- Log.i(LOG_TAG, String.format("Parsing test def file %s",
- testDefFile.getAbsolutePath()));
- parser.parse(new FileInputStream(testDefFile));
- } catch (FileNotFoundException e) {
- Log.e(LOG_TAG, String.format("Could not find test def file %s",
- testDefFile.getAbsolutePath()));
- } catch (ParseException e) {
- Log.e(LOG_TAG, String.format("Could not parse test def file %s: %s",
- testDefFile.getAbsolutePath(), e.getMessage()));
- }
- }
-
- /**
- * Run the previously built tests.
- *
- * @param listener the {@link ITestInvocationListener}
- * @throws DeviceNotAvailableException
- */
- private void doRun(ITestInvocationListener listener) throws DeviceNotAvailableException {
- while (!mTests.isEmpty()) {
- InstrumentationTest test = mTests.get(0);
-
- Log.d(LOG_TAG, String.format("Running test %s on %s", test.getPackageName(),
- getDevice().getSerialNumber()));
-
- if (mSendCoverage && test.getCoverageTarget() != null) {
- sendCoverage(test.getPackageName(), test.getCoverageTarget(), listener);
- }
- test.setDevice(getDevice());
- test.run(listener);
- // test completed, remove from list
- mTests.remove(0);
- }
- }
-
- /**
- * Forwards the tests coverage target info as a test metric.
- *
- * @param packageName
- * @param coverageTarget
- * @param listener
- */
- private void sendCoverage(String packageName, String coverageTarget,
- ITestInvocationListener listener) {
- HashMap<String, Metric> coverageMetric = new HashMap<>(1);
- coverageMetric.put(COVERAGE_TARGET_KEY, TfMetricProtoUtil.stringToMetric(coverageTarget));
- listener.testRunStarted(packageName, 0);
- listener.testRunEnded(0, coverageMetric);
- }
-
- /**
- * Retrieves a set of files from device into temporary files on local filesystem.
- *
- * @param remoteFilePaths
- */
- private Collection<File> getRemoteFile(Collection<String> remoteFilePaths)
- throws DeviceNotAvailableException {
- Collection<File> files = new ArrayList<File>();
- if (getDevice() == null) {
- Log.d(LOG_TAG, "Device not set, skipping collection of remote file");
- return files;
- }
- for (String remoteFilePath : remoteFilePaths) {
- try {
- File tmpFile = FileUtil.createTempFile("test_defs_", ".xml");
- getDevice().pullFile(remoteFilePath, tmpFile);
- files.add(tmpFile);
- } catch (IOException e) {
- Log.e(LOG_TAG, "Failed to create temp file");
- Log.e(LOG_TAG, e);
- }
- }
- return files;
- }
-
- void setShellTimeout(long timeout) {
- mShellTimeout = timeout;
- }
-
- long getShellTimeout() {
- return mShellTimeout;
- }
-
- int getTestTimeout() {
- return mTestTimeout;
- }
-
- String getTestSize() {
- return mTestSize;
- }
-
- /**
- * Creates the {@link XmlDefsParser} to use. Exposed for unit testing.
- */
- XmlDefsParser createParser() {
- return new XmlDefsParser();
- }
-
- /**
- * Creates the {@link InstrumentationTest} to use. Exposed for unit testing.
- */
- InstrumentationTest createInstrumentationTest() {
- return new InstrumentationTest();
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean isResumable() {
- // hack to not resume if tests were never run
- // TODO: fix this properly in TestInvocation
- if (mTests == null) {
- return false;
- }
- return mIsResumeMode;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Collection<IRemoteTest> split() {
- if (mLocalFiles.isEmpty()) {
- Log.w(LOG_TAG, "sharding is only supported if local xml files have been specified");
- return null;
- }
- if (mNumShards <= 1) {
- return null;
- }
-
- try {
- buildTests();
- } catch (DeviceNotAvailableException e) {
- // should never happen
- }
- if (mTests.size() <= 1) {
- Log.w(LOG_TAG, "no tests to shard!");
- return null;
- }
-
- // treat shardQueue as a circular queue, to sequentially distribute tests among shards
- Queue<IRemoteTest> shardQueue = new LinkedList<IRemoteTest>();
- // don't create more shards than the number of tests we have!
- for (int i = 0; i < mNumShards && i < mTests.size(); i++) {
- XmlDefsTest shard = new XmlDefsTest();
- shard.mTests = new LinkedList<InstrumentationTest>();
- shardQueue.add(shard);
- }
- while (!mTests.isEmpty()) {
- InstrumentationTest test = mTests.remove(0);
- XmlDefsTest shard = (XmlDefsTest)shardQueue.poll();
- shard.mTests.add(test);
- shardQueue.add(shard);
- }
- return shardQueue;
- }
-}
diff --git a/src/com/android/tradefed/util/BuildInfoUtil.java b/src/com/android/tradefed/util/BuildInfoUtil.java
new file mode 100644
index 0000000..345fae5
--- /dev/null
+++ b/src/com/android/tradefed/util/BuildInfoUtil.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceProperties;
+import com.android.tradefed.device.ITestDevice;
+
+/** A util class to help manipulate {@link IBuildInfo} */
+public class BuildInfoUtil {
+
+ /**
+ * Reads build attributes from device and use them to override the relevant build info fields
+ *
+ * <p>Note: because branch information is not stored on device as build attributes, the injected
+ * branch info will be the following fields concatenated via dashes:
+ *
+ * <ul>
+ * <li><code>ro.product.brand</code>
+ * <li><code>ro.product.name</code>
+ * <li><code>ro.product.vendor.device</code> (maybe different on older API levels)
+ * <li><code>ro.build.version.release</code>
+ * </ul>
+ *
+ * @param buildInfo the build info where device build attributes will be injected
+ * @param device the device to read build attributes from
+ * @param overrideBuildId instead of reading from device, override build id to this value;
+ * <code>null</code> for no override
+ * @param overrideBuildFlavor instead of reading from device, override build flavor to this
+ * value; <code>null</code> for no override
+ * @param overrideBuildBranch instead of concatenating device attributes as substitute for
+ * branch, override it to this value; <code>null</code> for no override
+ * @param overrideBuildAlias instead of reading from device, override build alias to this value;
+ * <code>null</code> for no override
+ * @throws DeviceNotAvailableException
+ */
+ public static void bootstrapDeviceBuildAttributes(
+ IBuildInfo buildInfo,
+ ITestDevice device,
+ String overrideBuildId,
+ String overrideBuildFlavor,
+ String overrideBuildBranch,
+ String overrideBuildAlias)
+ throws DeviceNotAvailableException {
+ String buildId, buildAlias, buildFlavor, branch;
+ // inject build id
+ if (overrideBuildId != null) {
+ buildId = overrideBuildId;
+ } else {
+ buildId = device.getBuildId();
+ }
+ buildInfo.setBuildId(buildId);
+
+ // inject build alias
+ if (overrideBuildAlias != null) {
+ buildAlias = overrideBuildAlias;
+ } else {
+ buildAlias = device.getBuildAlias();
+ }
+ buildInfo.addBuildAttribute("build_alias", buildAlias);
+
+ // inject build flavor
+ if (overrideBuildFlavor != null) {
+ buildFlavor = overrideBuildFlavor;
+ } else {
+ buildFlavor = device.getBuildFlavor();
+ }
+ buildInfo.setBuildFlavor(buildFlavor);
+
+ // generate branch information, either via parameter override, or via concatenating fields
+ if (overrideBuildBranch != null) {
+ branch = overrideBuildBranch;
+ } else {
+ branch =
+ String.format(
+ "%s-%s-%s-%s",
+ device.getProperty(DeviceProperties.BRAND),
+ device.getProperty(DeviceProperties.PRODUCT),
+ device.getProductVariant(),
+ device.getProperty(DeviceProperties.RELEASE_VERSION));
+ }
+ buildInfo.setBuildBranch(branch);
+ }
+}
diff --git a/src/com/android/tradefed/util/BundletoolUtil.java b/src/com/android/tradefed/util/BundletoolUtil.java
index 3e29b90..4bd117c 100644
--- a/src/com/android/tradefed/util/BundletoolUtil.java
+++ b/src/com/android/tradefed/util/BundletoolUtil.java
@@ -25,10 +25,13 @@
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
+import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
/**
* Utility class that uses bundletool command line to install the .apks on deivce. Bundletool doc
@@ -42,6 +45,7 @@
private static final String DEVICE_SPEC_OUTPUT_FLAG = "--output=";
private static final String APKS_TO_EXTRACT_FLAG = "--apks=";
private static final String DEVICE_SPEC_FLAG = "--device-spec=";
+ private static final String DEVICE_ID_FLAG = "--device-id=";
private static final String EXTRACT_APKS_OPTION = "extract-apks";
private static final String INSTALL_APKS_OPTION = "install-apks";
private static final String DEVICE_SPEC_FILE_EXTENSION = ".json";
@@ -55,7 +59,7 @@
mRunUtil = new RunUtil();
}
- public File getBundletoolFile() {
+ protected File getBundletoolFile() {
return mBundleToolFile;
}
@@ -65,33 +69,42 @@
* @param device the connected device
* @return a {@link String} representing the path of the device specification file.
*/
- public String generateDeviceSpecFile(ITestDevice device) {
+ public String generateDeviceSpecFile(ITestDevice device) throws IOException {
Path specFilePath =
Paths.get(
getBundletoolFile().getParentFile().getAbsolutePath(),
device.getSerialNumber() + DEVICE_SPEC_FILE_EXTENSION);
- if (Files.exists(specFilePath)) {
- return specFilePath.toString();
- }
+
+ Files.deleteIfExists(specFilePath);
String outputDirArg = DEVICE_SPEC_OUTPUT_FLAG + specFilePath.toString();
- String adbArg = "--adb=" + getAdbPath();
+ String deviceIdArg = DEVICE_ID_FLAG + device.getSerialNumber();
- String[] cmd =
- new String[] {
- "java",
- "-jar",
- getBundletoolFile().getAbsolutePath(),
- GET_DEVICE_SPEC_OPTION,
- outputDirArg,
- adbArg
- };
- CommandResult res = getRunUtil().runTimedCmd(CMD_TIME_OUT, cmd);
+ List<String> generateDeviceSpecCmd =
+ new ArrayList<String>(
+ Arrays.asList(
+ "java",
+ "-jar",
+ getBundletoolFile().getAbsolutePath(),
+ GET_DEVICE_SPEC_OPTION,
+ outputDirArg,
+ deviceIdArg));
+
+ if (getAdbPath() != null) {
+ generateDeviceSpecCmd.add("--adb=" + getAdbPath());
+ }
+
+ CommandResult res =
+ getRunUtil()
+ .runTimedCmd(
+ CMD_TIME_OUT,
+ generateDeviceSpecCmd.toArray(
+ new String[generateDeviceSpecCmd.size()]));
if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
CLog.e(
"Failed to generated device spec file. Cmd is %s. Error: %s.",
- Arrays.toString(cmd), res.getStderr());
+ generateDeviceSpecCmd.toString(), res.getStderr());
return null;
}
return specFilePath.toString();
@@ -151,22 +164,33 @@
*/
public void installApks(File apks, ITestDevice device) throws TargetSetupError {
String inputPathArg = "--apks=" + apks.getAbsolutePath();
- String adbArg = "--adb=" + getAdbPath();
- String[] installApksCmd =
- new String[] {
- "java",
- "-jar",
- getBundletoolFile().getAbsolutePath(),
- INSTALL_APKS_OPTION,
- inputPathArg,
- adbArg
- };
- CommandResult res = getRunUtil().runTimedCmd(CMD_TIME_OUT, installApksCmd);
+
+ String deviceIdArg = DEVICE_ID_FLAG + device.getSerialNumber();
+
+ List<String> installApksCmd =
+ new ArrayList<String>(
+ Arrays.asList(
+ "java",
+ "-jar",
+ getBundletoolFile().getAbsolutePath(),
+ INSTALL_APKS_OPTION,
+ inputPathArg,
+ deviceIdArg));
+
+ if (getAdbPath() != null) {
+ installApksCmd.add("--adb=" + getAdbPath());
+ }
+
+ CommandResult res =
+ getRunUtil()
+ .runTimedCmd(
+ CMD_TIME_OUT,
+ installApksCmd.toArray(new String[installApksCmd.size()]));
if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
throw new TargetSetupError(
String.format(
"Failed to install split apk. Cmd: %s. Error: %s.",
- Arrays.toString(installApksCmd), res.getStderr()),
+ installApksCmd.toString(), res.getStderr()),
device.getDeviceDescriptor());
}
CLog.i("%s is installed successfully", apks.getName());
@@ -180,6 +204,11 @@
@VisibleForTesting
protected String getAdbPath() {
- return GlobalConfiguration.getDeviceManagerInstance().getAdbPath();
+ String adbPath = GlobalConfiguration.getDeviceManagerInstance().getAdbPath();
+ // No explicit adb path passed from device manager.
+ if (!new File(adbPath).exists()) {
+ return null;
+ }
+ return adbPath;
}
}
diff --git a/src/com/android/tradefed/util/GoogleApiClientUtil.java b/src/com/android/tradefed/util/GoogleApiClientUtil.java
index e8a720c..187968a 100644
--- a/src/com/android/tradefed/util/GoogleApiClientUtil.java
+++ b/src/com/android/tradefed/util/GoogleApiClientUtil.java
@@ -224,6 +224,7 @@
private static class RetryResponseHandler implements HttpUnsuccessfulResponseHandler {
// Initial interval to wait before retrying if a request fails.
private static final int INITIAL_RETRY_INTERVAL = 1000;
+ private static final int MAX_RETRY_INTERVAL = 3 * 60000; // Set max interval to 3 minutes.
private final HttpUnsuccessfulResponseHandler backOffHandler;
@@ -232,6 +233,7 @@
new HttpBackOffUnsuccessfulResponseHandler(
new ExponentialBackOff.Builder()
.setInitialIntervalMillis(INITIAL_RETRY_INTERVAL)
+ .setMaxIntervalMillis(MAX_RETRY_INTERVAL)
.build());
}
diff --git a/src/com/android/tradefed/util/LocalRunInstructionBuilder.java b/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
index 8dec663..7117264 100644
--- a/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
+++ b/src/com/android/tradefed/util/LocalRunInstructionBuilder.java
@@ -16,12 +16,14 @@
package com.android.tradefed.util;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.ConfigurationDescriptor.LocalTestRunner;
+import com.android.tradefed.config.OptionDef;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.TestDescription;
+import java.util.List;
+
/** Utility to compile the instruction to run test locally. */
public class LocalRunInstructionBuilder {
@@ -106,6 +108,14 @@
instruction.append(" " + option);
}
}
+ // Ensure repro is aligned with parameterized modules.
+ List<String> paramMetadata =
+ configDescriptor.getMetaData(ConfigurationDescriptor.PARAMETER_KEY);
+ if (paramMetadata != null
+ && paramMetadata.size() > 0
+ && "instant".equals(paramMetadata.get(0))) {
+ instruction.append(" --instant");
+ }
return instruction.toString();
}
}
diff --git a/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java b/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java
new file mode 100644
index 0000000..b53542a
--- /dev/null
+++ b/src/com/android/tradefed/util/NativeCodeCoverageFlusher.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * A utility class that clears native coverage measurements and forces a flush of native coverage
+ * data from processes on the device.
+ */
+public final class NativeCodeCoverageFlusher {
+
+ private static final String COVERAGE_FLUSH_COMMAND_FORMAT = "kill -37 %s";
+ private static final String CLEAR_NATIVE_COVERAGE_FILES = "rm -rf /data/misc/trace/*";
+
+ private final ITestDevice mDevice;
+
+ public NativeCodeCoverageFlusher(ITestDevice device) {
+ mDevice = device;
+ }
+
+ /**
+ * Clears coverage measurements from disk on the device. Device must be in adb root.
+ *
+ * @throws DeviceNotAvailableException
+ */
+ public void clearCoverageMeasurements() throws DeviceNotAvailableException {
+ checkState(mDevice.isAdbRoot(), "adb root is required to clear coverage files.");
+ mDevice.executeShellCommand(CLEAR_NATIVE_COVERAGE_FILES);
+ }
+
+ /**
+ * Forces a flush of native coverage data from processes running on the device. Device must be
+ * in adb root.
+ *
+ * @param processNames the name of processes to target for flushing; if empty, flushes from all
+ * running native processes on the device.
+ * @throws DeviceNotAvailableException
+ */
+ public void forceCoverageFlush(List<String> processNames) throws DeviceNotAvailableException {
+ checkState(mDevice.isAdbRoot(), "adb root is required to flush native coverage data.");
+
+ if ((processNames == null) || processNames.isEmpty()) {
+ // Use the special pid -1 to trigger a coverage flush of all running processes.
+ mDevice.executeShellCommand(String.format(COVERAGE_FLUSH_COMMAND_FORMAT, "-1"));
+ } else {
+ // Look up the pid of the processes to send them the coverage flush signal.
+ StringJoiner pidString = new StringJoiner(" ");
+ for (String processName : processNames) {
+ String pid = mDevice.getProcessPid(processName);
+ if (pid == null) {
+ CLog.w("Did not find pid for process \"%s\".", processName);
+ } else {
+ pidString.add(pid);
+ }
+ }
+
+ if (pidString.length() > 0) {
+ mDevice.executeShellCommand(
+ String.format(COVERAGE_FLUSH_COMMAND_FORMAT, pidString.toString()));
+ }
+ }
+ }
+}
diff --git a/src/com/android/tradefed/util/ProcessInfo.java b/src/com/android/tradefed/util/ProcessInfo.java
deleted file mode 100644
index 2effb49..0000000
--- a/src/com/android/tradefed/util/ProcessInfo.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.util;
-
-/**
- * Used to store process related(USER, PID and NAME) information.
- */
-public class ProcessInfo {
-
- private String mUser;
- private int mPid;
- private String mName;
-
- /**
- * Constructs the process info object based on the user, process id and
- * name of the process.
- *
- * @param user username of process owner
- * @param pid process id number
- * @param name process name
- */
- ProcessInfo(String user, int pid, String name) {
- mUser = user;
- mPid = pid;
- mName = name;
- }
-
- /** Returns the username of the process's owner. */
- public String getUser() {
- return mUser;
- }
-
- /** Returns the process ID number. */
- public int getPid() {
- return mPid;
- }
-
- /** Returns the process name. */
- public String getName() {
- return mName;
- }
-
-}
-
diff --git a/src/com/android/tradefed/util/SubprocessEventHelper.java b/src/com/android/tradefed/util/SubprocessEventHelper.java
index f97defb..50028d9 100644
--- a/src/com/android/tradefed/util/SubprocessEventHelper.java
+++ b/src/com/android/tradefed/util/SubprocessEventHelper.java
@@ -65,24 +65,28 @@
public String mRunName = null;
public Integer mTestCount = null;
public Integer mAttempt = null;
+ public Long mStartTime = null;
/** Keep this constructor for legacy compatibility. */
public TestRunStartedEventInfo(String runName, int testCount) {
mRunName = runName;
mTestCount = testCount;
mAttempt = 0;
+ mStartTime = System.currentTimeMillis();
}
- public TestRunStartedEventInfo(String runName, int testCount, int attempt) {
+ public TestRunStartedEventInfo(String runName, int testCount, int attempt, long startTime) {
mRunName = runName;
mTestCount = testCount;
mAttempt = attempt;
+ mStartTime = startTime;
}
public TestRunStartedEventInfo(JSONObject jsonObject) throws JSONException {
mRunName = jsonObject.getString(RUNNAME_KEY);
mTestCount = jsonObject.getInt(TESTCOUNT_KEY);
mAttempt = jsonObject.optInt(ATTEMPT_KEY, 0);
+ mStartTime = jsonObject.optLong(START_TIME, System.currentTimeMillis());
}
@Override
@@ -98,6 +102,9 @@
if (mAttempt != null) {
tags.put(ATTEMPT_KEY, mAttempt.intValue());
}
+ if (mStartTime != null) {
+ tags.put(START_TIME, mStartTime.longValue());
+ }
} catch (JSONException e) {
CLog.e(e);
}
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index b72690c..9bd7cbc 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -338,8 +338,9 @@
@Override
public void handleEvent(String eventJson) throws JSONException {
TestRunStartedEventInfo rsi = new TestRunStartedEventInfo(new JSONObject(eventJson));
- if (rsi.mAttempt != null && rsi.mAttempt != 0) {
- mListener.testRunStarted(rsi.mRunName, rsi.mTestCount, rsi.mAttempt);
+ if (rsi.mAttempt != null) {
+ mListener.testRunStarted(
+ rsi.mRunName, rsi.mTestCount, rsi.mAttempt, rsi.mStartTime);
} else {
mListener.testRunStarted(rsi.mRunName, rsi.mTestCount);
}
@@ -477,8 +478,8 @@
LogAssociationEventInfo assosInfo =
new LogAssociationEventInfo(new JSONObject(eventJson));
if (mListener instanceof ILogSaverListener) {
- ((ILogSaverListener) mListener)
- .logAssociation(assosInfo.mDataName, assosInfo.mLoggedFile);
+ String name = String.format("subprocess-%s", assosInfo.mDataName);
+ ((ILogSaverListener) mListener).logAssociation(name, assosInfo.mLoggedFile);
}
}
}
diff --git a/src/com/android/tradefed/util/TarUtil.java b/src/com/android/tradefed/util/TarUtil.java
index d9dfb2a..672696f 100644
--- a/src/com/android/tradefed/util/TarUtil.java
+++ b/src/com/android/tradefed/util/TarUtil.java
@@ -81,6 +81,15 @@
}
} else {
CLog.i(String.format("Creating output file %s.", outputFile.getAbsolutePath()));
+ final File parent = outputFile.getParentFile();
+ if (parent != null && !parent.exists()) {
+ if (!parent.mkdirs()) {
+ throw new IOException(
+ String.format(
+ "Couldn't create directory %s.",
+ parent.getAbsolutePath()));
+ }
+ }
final OutputStream outputFileStream = new FileOutputStream(outputFile);
IOUtils.copy(debInputStream, outputFileStream);
StreamUtil.close(outputFileStream);
@@ -153,6 +162,33 @@
}
/**
+ * Untar and ungzip a tar.gz file to a temp directory.
+ *
+ * @param targzFile the tar.gz file to extract.
+ * @param nameHint the prefix for the temp directory.
+ * @return the temp directory.
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public static File extractTarGzipToTemp(File targzFile, String nameHint)
+ throws FileNotFoundException, IOException {
+ File unGzipDir = null;
+ File unTarDir = null;
+ try {
+ unGzipDir = FileUtil.createTempDir("extractTarGzip");
+ File tarFile = TarUtil.unGzip(targzFile, unGzipDir);
+ unTarDir = FileUtil.createTempDir(nameHint);
+ TarUtil.unTar(tarFile, unTarDir);
+ return unTarDir;
+ } catch (IOException e) {
+ FileUtil.recursiveDelete(unTarDir);
+ throw e;
+ } finally {
+ FileUtil.recursiveDelete(unGzipDir);
+ }
+ }
+
+ /**
* Helper to extract and log to the reporters a tar gz file and its content
*
* @param listener the {@link ITestLogger} where to log the files.
diff --git a/src/com/android/tradefed/util/UserUtil.java b/src/com/android/tradefed/util/UserUtil.java
deleted file mode 100644
index cdab730..0000000
--- a/src/com/android/tradefed/util/UserUtil.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.util;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.log.LogUtil.CLog;
-
-public class UserUtil {
- // From the UserInfo class.
- public static final int FLAG_PRIMARY = 0x00000001;
- public static final int FLAG_GUEST = 0x00000004;
- public static final int FLAG_RESTRICTED = 0x00000008;
- public static final int FLAG_MANAGED_PROFILE = 0x00000020;
- public static final int USER_SYSTEM = 0;
-
- public static final int FLAGS_NOT_SECONDARY =
- FLAG_PRIMARY | FLAG_MANAGED_PROFILE | FLAG_GUEST | FLAG_RESTRICTED;
-
- /** Thrown if a user switch could not happen. */
- public static class UserSwitchFailedException extends Exception {
- public UserSwitchFailedException(String message) {
- super(message);
- }
- }
-
- /** Thrown if a user switch could not happen because the secondary user could not be found. */
- public static class SecondaryUserNotFoundException extends UserSwitchFailedException {
- public SecondaryUserNotFoundException() {
- super("Secondary User Not Found");
- }
- }
-
- /** Parameters that specify which user to run the test module as. */
- public enum UserType {
- // TODO:(b/123077733) Add support for guest
-
- /** current foreground user of the device */
- CURRENT,
- /** user flagged as primary on the device; most often primary = system user = user 0 */
- PRIMARY,
- /** system user = user 0 */
- SYSTEM,
- /** secondary user, i.e. non-primary and non-system. */
- SECONDARY,
- }
-
- /**
- * Attempt to switch to a user type.
- *
- * @returns true if successful, false if not.
- */
- public static void switchToUserType(ITestDevice device, UserType userType)
- throws DeviceNotAvailableException, UserSwitchFailedException {
- switch (userType) {
- case CURRENT:
- return; // do nothing
- case SYSTEM:
- switchUser(device, USER_SYSTEM);
- return;
- case PRIMARY:
- switchUser(device, device.getPrimaryUserId());
- return;
- case SECONDARY:
- switchToSecondaryUser(device);
- return;
- default:
- throw new RuntimeException("userType case not covered: " + userType);
- }
- }
-
- /**
- * Attempt to switch to a secondary user, creating one if necessary.
- *
- * @returns true if successful, false if not.
- */
- private static void switchToSecondaryUser(ITestDevice device)
- throws DeviceNotAvailableException, UserSwitchFailedException {
- int currentUser = device.getCurrentUser();
- if (device.isUserSecondary(currentUser)) {
- CLog.d("currentUser is already secondary, no action.");
- return;
- }
-
- int secondary = findExistingSecondary(device);
- if (secondary <= 0) {
- throw new SecondaryUserNotFoundException();
- }
-
- switchUser(device, secondary);
- }
-
- private static void switchUser(ITestDevice device, int userId)
- throws DeviceNotAvailableException, UserSwitchFailedException {
- if (!device.switchUser(userId)) {
- throw new UserSwitchFailedException("Failed to switch to user " + userId);
- }
- }
-
- /**
- * Finds an arbitrary secondary user and returns the userId.
- *
- * <p>TODO: evaluate if a more comprehensive API is needed for this or not.
- *
- * @return id of the secondary user or -1 if one could not be found.
- * @throws DeviceNotAvailableException
- */
- private static int findExistingSecondary(ITestDevice device)
- throws DeviceNotAvailableException {
- for (int userId : device.listUsers()) {
- if (device.isUserSecondary(userId)) {
- return userId;
- }
- }
- // Returns a negative id if we couldn't find a proper existing secondary user.
- return -1;
- }
-}
diff --git a/src/com/android/tradefed/util/ZipUtil.java b/src/com/android/tradefed/util/ZipUtil.java
deleted file mode 100644
index 61f46a2..0000000
--- a/src/com/android/tradefed/util/ZipUtil.java
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.util;
-
-import com.android.tradefed.log.LogUtil.CLog;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Enumeration;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.zip.GZIPOutputStream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipException;
-import java.util.zip.ZipFile;
-import java.util.zip.ZipOutputStream;
-
-/**
- * A helper class for compression-related operations
- */
-public class ZipUtil {
-
- private static final String DEFAULT_DIRNAME = "dir";
- private static final String DEFAULT_FILENAME = "files";
- private static final String ZIP_EXTENSION = ".zip";
-
- /**
- * Utility method to verify that a zip file is not corrupt.
- *
- * @param zipFile the {@link File} to check
- * @param thorough Whether to attempt to fully extract the archive. If {@code false}, this
- * method will fail to detect CRC errors in a well-formed archive.
- * @throws IOException if the file could not be opened or read
- * @return {@code false} if the file appears to be corrupt; {@code true} otherwise
- */
- public static boolean isZipFileValid(File zipFile, boolean thorough) throws IOException {
- if (zipFile != null && !zipFile.exists()) {
- CLog.d("Zip file does not exist: %s", zipFile.getAbsolutePath());
- return false;
- }
-
- try (ZipFile z = new ZipFile(zipFile)) {
- if (thorough) {
- // Reading the entire file is the only way to detect CRC errors within the archive
- final File extractDir = FileUtil.createTempDir("extract-" + zipFile.getName());
- try {
- extractZip(z, extractDir);
- } finally {
- FileUtil.recursiveDelete(extractDir);
- }
- }
- } catch (ZipException e) {
- // File is likely corrupted
- CLog.d("Detected corrupt zip file %s:", zipFile.getCanonicalPath());
- CLog.e(e);
- return false;
- }
-
- return true;
- }
-
- /**
- * Utility method to extract entire contents of zip file into given directory
- *
- * @param zipFile the {@link ZipFile} to extract
- * @param destDir the local dir to extract file to
- * @throws IOException if failed to extract file
- */
- public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
- Enumeration<? extends ZipEntry> entries = zipFile.entries();
- while (entries.hasMoreElements()) {
-
- ZipEntry entry = entries.nextElement();
- File childFile = new File(destDir, entry.getName());
- childFile.getParentFile().mkdirs();
- if (entry.isDirectory()) {
- continue;
- } else {
- FileUtil.writeToFile(zipFile.getInputStream(entry), childFile);
- }
- }
- }
-
- /**
- * Utility method to extract one specific file from zip file into a tmp file
- *
- * @param zipFile the {@link ZipFile} to extract
- * @param filePath the filePath of to extract
- * @throws IOException if failed to extract file
- * @return the {@link File} or null if not found
- */
- public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
- ZipEntry entry = zipFile.getEntry(filePath);
- if (entry == null) {
- return null;
- }
- File createdFile = FileUtil.createTempFile("extracted",
- FileUtil.getExtension(filePath));
- FileUtil.writeToFile(zipFile.getInputStream(entry), createdFile);
- return createdFile;
- }
-
- /**
- * Utility method to create a temporary zip file containing the given directory and
- * all its contents.
- *
- * @param dir the directory to zip
- * @return a temporary zip {@link File} containing directory contents
- * @throws IOException if failed to create zip file
- */
- public static File createZip(File dir) throws IOException {
- return createZip(dir, DEFAULT_DIRNAME);
- }
-
- /**
- * Utility method to create a temporary zip file containing the given directory and
- * all its contents.
- *
- * @param dir the directory to zip
- * @param name the base name of the zip file created without the extension.
- * @return a temporary zip {@link File} containing directory contents
- * @throws IOException if failed to create zip file
- */
- public static File createZip(File dir, String name) throws IOException {
- File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
- createZip(dir, zipFile);
- return zipFile;
- }
-
- /**
- * Utility method to create a zip file containing the given directory and
- * all its contents.
- *
- * @param dir the directory to zip
- * @param zipFile the zip file to create - it should not already exist
- * @throws IOException if failed to create zip file
- */
- public static void createZip(File dir, File zipFile) throws IOException {
- ZipOutputStream out = null;
- try {
- FileOutputStream fileStream = new FileOutputStream(zipFile);
- out = new ZipOutputStream(new BufferedOutputStream(fileStream));
- addToZip(out, dir, new LinkedList<String>());
- } catch (IOException e) {
- zipFile.delete();
- throw e;
- } catch (RuntimeException e) {
- zipFile.delete();
- throw e;
- } finally {
- StreamUtil.close(out);
- }
- }
-
- /**
- * Utility method to create a temporary zip file containing the given files
- *
- * @param files list of files to zip
- * @return a temporary zip {@link File} containing directory contents
- * @throws IOException if failed to create zip file
- */
- public static File createZip(List<File> files) throws IOException {
- return createZip(files, DEFAULT_FILENAME);
- }
-
- /**
- * Utility method to create a temporary zip file containing the given files.
- *
- * @param files list of files to zip
- * @param name the base name of the zip file created without the extension.
- * @return a temporary zip {@link File} containing directory contents
- * @throws IOException if failed to create zip file
- */
- public static File createZip(List<File> files, String name) throws IOException {
- File zipFile = FileUtil.createTempFile(name, ZIP_EXTENSION);
- createZip(files, zipFile);
- return zipFile;
- }
-
- /**
- * Utility method to create a zip file containing the given files
- *
- * @param files list of files to zip
- * @param zipFile the zip file to create - it should not already exist
- * @throws IOException if failed to create zip file
- */
- public static void createZip(List<File> files, File zipFile) throws IOException {
- ZipOutputStream out = null;
- try {
- FileOutputStream fileStream = new FileOutputStream(zipFile);
- out = new ZipOutputStream(new BufferedOutputStream(fileStream));
- for (File file : files) {
- addToZip(out, file, new LinkedList<String>());
- }
- } catch (IOException|RuntimeException e) {
- zipFile.delete();
- throw e;
- } finally {
- StreamUtil.close(out);
- }
- }
-
- /**
- * Recursively adds given file and its contents to ZipOutputStream
- *
- * @param out the {@link ZipOutputStream}
- * @param file the {@link File} to add to the stream
- * @param relativePathSegs the relative path of file, including separators
- * @throws IOException if failed to add file to zip
- */
- public static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs)
- throws IOException {
- relativePathSegs.add(file.getName());
- if (file.isDirectory()) {
- // note: it appears even on windows, ZipEntry expects '/' as a path separator
- relativePathSegs.add("/");
- }
- ZipEntry zipEntry = new ZipEntry(buildPath(relativePathSegs));
- out.putNextEntry(zipEntry);
- if (file.isFile()) {
- writeToStream(file, out);
- }
- out.closeEntry();
- if (file.isDirectory()) {
- // recursively add contents
- File[] subFiles = file.listFiles();
- if (subFiles == null) {
- throw new IOException(String.format("Could not read directory %s",
- file.getAbsolutePath()));
- }
- for (File subFile : subFiles) {
- addToZip(out, subFile, relativePathSegs);
- }
- // remove the path separator
- relativePathSegs.remove(relativePathSegs.size()-1);
- }
- // remove the last segment, added at beginning of method
- relativePathSegs.remove(relativePathSegs.size()-1);
- }
-
- /**
- * Close an open {@link ZipFile}, ignoring any exceptions.
- *
- * @param zipFile the file to close
- */
- public static void closeZip(ZipFile zipFile) {
- if (zipFile != null) {
- try {
- zipFile.close();
- } catch (IOException e) {
- // ignore
- }
- }
- }
-
- /**
- * Helper method to create a gzipped version of a single file.
- *
- * @param file the original file
- * @param gzipFile the file to place compressed contents in
- * @throws IOException
- */
- public static void gzipFile(File file, File gzipFile) throws IOException {
- GZIPOutputStream out = null;
- try {
- FileOutputStream fileStream = new FileOutputStream(gzipFile);
- out = new GZIPOutputStream(new BufferedOutputStream(fileStream, 64 * 1024));
- writeToStream(file, out);
- } catch (IOException e) {
- gzipFile.delete();
- throw e;
- } catch (RuntimeException e) {
- gzipFile.delete();
- throw e;
- } finally {
- StreamUtil.close(out);
- }
- }
-
- /**
- * Helper method to write input file contents to output stream.
- *
- * @param file the input {@link File}
- * @param out the {@link OutputStream}
- *
- * @throws IOException
- */
- private static void writeToStream(File file, OutputStream out) throws IOException {
- InputStream inputStream = null;
- try {
- inputStream = new BufferedInputStream(new FileInputStream(file));
- StreamUtil.copyStreams(inputStream, out);
- } finally {
- StreamUtil.close(inputStream);
- }
- }
-
- /**
- * Builds a file system path from a stack of relative path segments
- *
- * @param relativePathSegs the list of relative paths
- * @return a {@link String} containing all relativePathSegs
- */
- private static String buildPath(List<String> relativePathSegs) {
- StringBuilder pathBuilder = new StringBuilder();
- for (String segment : relativePathSegs) {
- pathBuilder.append(segment);
- }
- return pathBuilder.toString();
- }
-
- /**
- * Extract a zip file to a temp directory prepended with a string
- *
- * @param zipFile the zip file to extract
- * @param nameHint a prefix for the temp directory
- * @return a {@link File} pointing to the temp directory
- */
- public static File extractZipToTemp(File zipFile, String nameHint)
- throws IOException, ZipException {
- File localRootDir = FileUtil.createTempDir(nameHint);
- try (ZipFile zip = new ZipFile(zipFile)) {
- extractZip(zip, localRootDir);
- return localRootDir;
- } catch (IOException e) {
- // clean tmp file since we couldn't extract.
- FileUtil.recursiveDelete(localRootDir);
- throw e;
- }
- }
-}
diff --git a/src/com/android/tradefed/util/sl4a/Sl4aClient.java b/src/com/android/tradefed/util/sl4a/Sl4aClient.java
index a9954bb..b066c91 100644
--- a/src/com/android/tradefed/util/sl4a/Sl4aClient.java
+++ b/src/com/android/tradefed/util/sl4a/Sl4aClient.java
@@ -191,7 +191,7 @@
mSocket = new Socket("localhost", mHostPort);
CLog.i("is sl4a socket connected: %s", mSocket.isConnected());
String rep = sendCommand(Sl4aClient.INIT);
- CLog.i("response sl4a INIT: %s", rep);
+ CLog.i("response sl4a INIT: '%s', from device %s", rep, mDevice.getSerialNumber());
JSONObject init = new JSONObject(rep);
mUid = init.getInt("uid");
startEventDispatcher();
@@ -223,11 +223,11 @@
PrintWriter out = new PrintWriter(mSocket.getOutputStream(), true);
out.print(message.toString());
out.print('\n');
- CLog.i("flushing");
+ CLog.d("flushing");
out.flush();
- CLog.i("sent");
+ CLog.d("sent");
BufferedReader in = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
- CLog.i("reading");
+ CLog.d("reading");
String response = in.readLine();
return response;
}
@@ -240,14 +240,14 @@
* @throws IOException
*/
private synchronized Object sendThroughSocket(String message) throws IOException {
- CLog.d("preparing sending: '%s'", message.toString());
+ CLog.d("preparing sending: '%s' to device %s", message, mDevice.getSerialNumber());
PrintWriter out = new PrintWriter(mSocket.getOutputStream(), false);
- out.print(message.toString());
+ out.print(message);
out.print('\n');
out.flush();
BufferedReader in = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
String response = in.readLine();
- CLog.d("response: '%s'", response);
+ CLog.d("response: '%s' from device %s", response, mDevice.getSerialNumber());
try {
JSONObject resp = new JSONObject(response);
if (!resp.isNull("error")) {
diff --git a/src/com/android/tradefed/util/testmapping/TestMapping.java b/src/com/android/tradefed/util/testmapping/TestMapping.java
index 79da7c2..fdc019d 100644
--- a/src/com/android/tradefed/util/testmapping/TestMapping.java
+++ b/src/com/android/tradefed/util/testmapping/TestMapping.java
@@ -42,6 +42,8 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -63,6 +65,10 @@
private static final String DISABLED_PRESUBMIT_TESTS_FILE = "disabled-presubmit-tests";
private Map<String, Set<TestInfo>> mTestCollection = null;
+ // Pattern used to identify comments start with "//" or "#" in TEST_MAPPING.
+ private static final Pattern COMMENTS_REGEX = Pattern.compile(
+ "(?m)[\\s\\t]*(//|#).*|(\".*?\")");
+ private static final Set<String> COMMENTS = new HashSet<>(Arrays.asList("#", "//"));
/**
* Constructor to create a {@link TestMapping} object from a path to TEST_MAPPING file.
@@ -75,7 +81,8 @@
String relativePath = testMappingsDir.relativize(path.getParent()).toString();
String errorMessage = null;
try {
- String content = String.join("", Files.readAllLines(path, StandardCharsets.UTF_8));
+ String content = removeComments(
+ String.join("\n", Files.readAllLines(path, StandardCharsets.UTF_8)));
if (content != null) {
JSONTokener tokener = new JSONTokener(content);
JSONObject root = new JSONObject(tokener);
@@ -139,6 +146,26 @@
}
/**
+ * Helper to remove comments in a TEST_MAPPING file to valid format. Only "//" and "#" are
+ * regarded as comments.
+ *
+ * @param jsonContent A {@link String} of json which content is from a TEST_MAPPING file.
+ * @return A {@link String} of valid json without comments.
+ */
+ @VisibleForTesting
+ static String removeComments(String jsonContent) {
+ StringBuffer out = new StringBuffer();
+ Matcher matcher = COMMENTS_REGEX.matcher(jsonContent);
+ while (matcher.find()) {
+ if (COMMENTS.contains(matcher.group(1))) {
+ matcher.appendReplacement(out, "");
+ }
+ }
+ matcher.appendTail(out);
+ return out.toString();
+ }
+
+ /**
* Helper to get all tests set in a TEST_MAPPING file for a given group.
*
* @param testGroup A {@link String} of the test group.
diff --git a/src/com/android/tradefed/util/xml/AndroidManifestWriter.java b/src/com/android/tradefed/util/xml/AndroidManifestWriter.java
deleted file mode 100644
index 4f72f14..0000000
--- a/src/com/android/tradefed/util/xml/AndroidManifestWriter.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
- * may not use this file except in compliance with the License. You may obtain a
- * copy of the License at
- *
- * http://www.eclipse.org/org/documents/epl-v10.php
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.tradefed.util.xml;
-
-import com.android.tradefed.log.LogUtil.CLog;
-
-import org.w3c.dom.Attr;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
-import java.io.File;
-import java.io.IOException;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.Result;
-import javax.xml.transform.Source;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-
-/**
- * Helper class for modifying an AndroidManifest.
- * <p/>
- * copied from
- * <android source>/platform/sdk/.../adt-tests/com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper.
- * TODO: Find a way of sharing this code with adt-tests
- */
-public class AndroidManifestWriter {
-
- private final Document mDoc;
- private final String mOsManifestFilePath;
-
- private static final String NODE_USES_SDK = "uses-sdk";
- private static final String ATTRIBUTE_MIN_SDK_VERSION = "minSdkVersion";
- /** Namespace for the resource XML, i.e. "http://schemas.android.com/apk/res/android" */
- private final static String NS_RESOURCES = "http://schemas.android.com/apk/res/android";
-
- private AndroidManifestWriter(Document doc, String osManifestFilePath) {
- mDoc = doc;
- mOsManifestFilePath = osManifestFilePath;
- }
-
- /**
- * Sets the minimum SDK version for this manifest.
- *
- * @param minSdkVersion - the minimim sdk version to use
- * @return <code>true</code> on success, false otherwise
- */
- public boolean setMinSdkVersion(String minSdkVersion) {
- Element usesSdkElement = null;
- NodeList nodeList = mDoc.getElementsByTagName(NODE_USES_SDK);
- if (nodeList.getLength() > 0) {
- usesSdkElement = (Element) nodeList.item(0);
- } else {
- usesSdkElement = mDoc.createElement(NODE_USES_SDK);
- mDoc.getDocumentElement().appendChild(usesSdkElement);
- }
- Attr minSdkAttr = mDoc.createAttributeNS(NS_RESOURCES, ATTRIBUTE_MIN_SDK_VERSION);
- String prefix = mDoc.lookupPrefix(NS_RESOURCES);
- minSdkAttr.setPrefix(prefix);
- minSdkAttr.setValue(minSdkVersion);
- usesSdkElement.setAttributeNodeNS(minSdkAttr);
- return saveXmlToFile();
- }
-
- private boolean saveXmlToFile() {
- try {
- // Prepare the DOM document for writing
- Source source = new DOMSource(mDoc);
-
- // Prepare the output file
- File file = new File(mOsManifestFilePath);
- Result result = new StreamResult(file);
-
- // Write the DOM document to the file
- Transformer xformer = TransformerFactory.newInstance().newTransformer();
- xformer.transform(source, result);
- } catch (TransformerConfigurationException e) {
- CLog.e("Failed to write xml file %s", mOsManifestFilePath);
- CLog.e(e);
- return false;
- } catch (TransformerException e) {
- CLog.e("Failed to write xml file %s", mOsManifestFilePath);
- CLog.e(e);
- return false;
- }
- return true;
- }
-
- /**
- * Parses the manifest file, and collects data.
- *
- * @param osManifestFilePath The OS path of the manifest file to parse.
- * @return an {@link AndroidManifestWriter} or null if parsing failed
- */
- public static AndroidManifestWriter parse(String osManifestFilePath) {
- try {
- DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
- docFactory.setNamespaceAware(true);
- DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
- docBuilder.setErrorHandler(new DefaultHandler());
- Document doc = docBuilder.parse(osManifestFilePath);
- return new AndroidManifestWriter(doc, osManifestFilePath);
- } catch (ParserConfigurationException e) {
- CLog.e("Error parsing file %s", osManifestFilePath);
- CLog.e(e);
- return null;
- } catch (SAXException e) {
- CLog.e("Error parsing file %s", osManifestFilePath);
- CLog.e(e);
- return null;
- } catch (IOException e) {
- CLog.e("Error parsing file %s", osManifestFilePath);
- CLog.e(e);
- return null;
- }
- }
-}
diff --git a/test_framework/Android.bp b/test_framework/Android.bp
new file mode 100644
index 0000000..561d9a8
--- /dev/null
+++ b/test_framework/Android.bp
@@ -0,0 +1,25 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+ name: "tradefed-test-framework",
+ defaults: ["tradefed_defaults"],
+ srcs: [
+ "com/**/*.java",
+ ],
+ libs: [
+ "tradefed-lib-core",
+ ],
+}
+
diff --git a/src/com/android/tradefed/testtype/PythonUnitTestResultParser.java b/test_framework/com/android/tradefed/testtype/PythonUnitTestResultParser.java
similarity index 100%
rename from src/com/android/tradefed/testtype/PythonUnitTestResultParser.java
rename to test_framework/com/android/tradefed/testtype/PythonUnitTestResultParser.java
diff --git a/src/com/android/tradefed/testtype/PythonUnitTestRunner.java b/test_framework/com/android/tradefed/testtype/PythonUnitTestRunner.java
similarity index 100%
rename from src/com/android/tradefed/testtype/PythonUnitTestRunner.java
rename to test_framework/com/android/tradefed/testtype/PythonUnitTestRunner.java
diff --git a/src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
similarity index 95%
rename from src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
rename to test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
index 0185d23..302fca2 100644
--- a/src/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableBaseTest.java
@@ -27,8 +27,10 @@
import com.android.tradefed.testtype.IRuntimeHintProvider;
import com.android.tradefed.testtype.IShardableTest;
import com.android.tradefed.testtype.ITestCollector;
+import com.android.tradefed.util.StreamUtil;
import java.io.File;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -77,6 +79,8 @@
// Do not actually run the test if we are dry running it.
runBinary(path, listener, description);
}
+ } catch (IOException e) {
+ listener.testFailed(description, StreamUtil.getStackTrace(e));
} finally {
listener.testEnded(description, new HashMap<String, Metric>());
listener.testRunEnded(
@@ -104,7 +108,7 @@
*/
public abstract void runBinary(
String binaryPath, ITestInvocationListener listener, TestDescription description)
- throws DeviceNotAvailableException;
+ throws DeviceNotAvailableException, IOException;
/** {@inheritDoc} */
@Override
diff --git a/src/com/android/tradefed/testtype/binary/ExecutableHostTest.java b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
similarity index 67%
rename from src/com/android/tradefed/testtype/binary/ExecutableHostTest.java
rename to test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
index b22cb7f..949344a 100644
--- a/src/com/android/tradefed/testtype/binary/ExecutableHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/binary/ExecutableHostTest.java
@@ -24,8 +24,11 @@
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.StubDevice;
+import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.testtype.IDeviceTest;
@@ -36,6 +39,7 @@
import com.android.tradefed.util.RunUtil;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -49,6 +53,8 @@
public class ExecutableHostTest extends ExecutableBaseTest implements IDeviceTest, IBuildReceiver {
private static final String ANDROID_SERIAL = "ANDROID_SERIAL";
+ private static final String LOG_STDOUT_TAG = "-binary-stdout-";
+ private static final String LOG_STDERR_TAG = "-binary-stderr-";
@Option(
name = "per-binary-timeout",
@@ -57,6 +63,14 @@
)
private long mTimeoutPerBinaryMs = 5 * 60 * 1000L;
+ @Option(
+ name = "relative-path-execution",
+ description =
+ "Some scripts assume a relative location to their tests file, this allows to"
+ + " execute with that relative location."
+ )
+ private boolean mExecuteRelativeToScript = false;
+
private ITestDevice mDevice;
private IBuildInfo mBuild;
@@ -97,7 +111,7 @@
@Override
public void runBinary(
String binaryPath, ITestInvocationListener listener, TestDescription description)
- throws DeviceNotAvailableException {
+ throws DeviceNotAvailableException, IOException {
IRunUtil runUtil = createRunUtil();
// Output everything in stdout
runUtil.setRedirectStderrToStdout(true);
@@ -109,20 +123,42 @@
FileUtil.chmodRWXRecursively(new File(binaryPath));
List<String> command = new ArrayList<>();
- command.add(binaryPath);
- CommandResult res =
- runUtil.runTimedCmd(mTimeoutPerBinaryMs, command.toArray(new String[0]));
- if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
- // Everything should be outputted in stdout with our redirect above.
- String errorMessage = res.getStdout();
- if (CommandStatus.TIMED_OUT.equals(res.getStatus())) {
- errorMessage += "\nTimeout.";
- }
- if (res.getExitCode() != null) {
- errorMessage += String.format("\nExit Code: %s", res.getExitCode());
- }
- listener.testFailed(description, errorMessage);
+ String scriptName = new File(binaryPath).getName();
+ if (mExecuteRelativeToScript) {
+ String parentDir = new File(binaryPath).getParent();
+ command.add("bash");
+ command.add("-c");
+ command.add(String.format("pushd %s; ./%s;", parentDir, scriptName));
+ } else {
+ command.add(binaryPath);
}
+ File stdout = FileUtil.createTempFile(scriptName + LOG_STDOUT_TAG, ".txt");
+ File stderr = FileUtil.createTempFile(scriptName + LOG_STDERR_TAG, ".txt");
+
+ try (FileOutputStream stdoutStream = new FileOutputStream(stdout);
+ FileOutputStream stderrStream = new FileOutputStream(stderr); ) {
+ CommandResult res =
+ runUtil.runTimedCmd(
+ mTimeoutPerBinaryMs,
+ stdoutStream,
+ stderrStream,
+ command.toArray(new String[0]));
+ if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
+ // Everything should be outputted in stdout with our redirect above.
+ String errorMessage = FileUtil.readStringFromFile(stdout);
+ if (CommandStatus.TIMED_OUT.equals(res.getStatus())) {
+ errorMessage += "\nTimeout.";
+ }
+ if (res.getExitCode() != null) {
+ errorMessage += String.format("\nExit Code: %s", res.getExitCode());
+ }
+ listener.testFailed(description, errorMessage);
+ }
+ } finally {
+ logFile(stdout, listener);
+ logFile(stderr, listener);
+ }
+
if (!(mDevice.getIDevice() instanceof StubDevice)) {
// Ensure that the binary did not leave the device offline.
CLog.d("Checking whether device is still online after %s", binaryPath);
@@ -155,4 +191,10 @@
IRunUtil createRunUtil() {
return new RunUtil();
}
+
+ private void logFile(File logFile, ITestLogger logger) {
+ try (FileInputStreamSource source = new FileInputStreamSource(logFile, true)) {
+ logger.testLog(logFile.getName(), LogDataType.TEXT, source);
+ }
+ }
}
diff --git a/src/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java b/test_framework/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
similarity index 100%
rename from src/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
rename to test_framework/com/android/tradefed/testtype/host/CoverageMeasurementForwarder.java
diff --git a/src/com/android/tradefed/testtype/python/PythonBinaryHostTest.java b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
similarity index 92%
rename from src/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
rename to test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
index 3a9ab1a..0286ea5 100644
--- a/src/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
+++ b/test_framework/com/android/tradefed/testtype/python/PythonBinaryHostTest.java
@@ -177,12 +177,16 @@
File updatedAdb = mBuildInfo.getFile(AdbStopServerPreparer.ADB_BINARY_KEY);
if (updatedAdb == null) {
String adbPath = getAdbPath();
- updatedAdb = new File(adbPath);
- if (!updatedAdb.exists()) {
- updatedAdb = null;
+ // Don't check if it's the adb on the $PATH
+ if (!adbPath.equals("adb")) {
+ updatedAdb = new File(adbPath);
+ if (!updatedAdb.exists()) {
+ updatedAdb = null;
+ }
}
}
if (updatedAdb != null) {
+ CLog.d("Testing with adb binary at: %s", updatedAdb);
// If a special adb version is used, pass it to the PATH
CommandResult pathResult =
getRunUtil()
@@ -289,7 +293,19 @@
@Override
public void testRunStarted(String runName, int testCount) {
// Replace run name
- super.testRunStarted(mRunName, testCount);
+ testRunStarted(runName, testCount, 0);
+ }
+
+ @Override
+ public void testRunStarted(String runName, int testCount, int attempt) {
+ // Replace run name
+ testRunStarted(runName, testCount, attempt, System.currentTimeMillis());
+ }
+
+ @Override
+ public void testRunStarted(String runName, int testCount, int attempt, long startTime) {
+ // Replace run name
+ super.testRunStarted(mRunName, testCount, attempt, startTime);
}
}
}
diff --git a/src/com/android/tradefed/testtype/suite/module/CarModuleController.java b/test_framework/com/android/tradefed/testtype/suite/module/CarModuleController.java
similarity index 100%
rename from src/com/android/tradefed/testtype/suite/module/CarModuleController.java
rename to test_framework/com/android/tradefed/testtype/suite/module/CarModuleController.java
diff --git a/src/com/android/tradefed/testtype/suite/module/NativeBridgeModuleController.java b/test_framework/com/android/tradefed/testtype/suite/module/NativeBridgeModuleController.java
similarity index 100%
rename from src/com/android/tradefed/testtype/suite/module/NativeBridgeModuleController.java
rename to test_framework/com/android/tradefed/testtype/suite/module/NativeBridgeModuleController.java
diff --git a/src/com/android/tradefed/testtype/suite/module/TestFailureModuleController.java b/test_framework/com/android/tradefed/testtype/suite/module/TestFailureModuleController.java
similarity index 100%
rename from src/com/android/tradefed/testtype/suite/module/TestFailureModuleController.java
rename to test_framework/com/android/tradefed/testtype/suite/module/TestFailureModuleController.java
diff --git a/test_result_interfaces/Android.bp b/test_result_interfaces/Android.bp
new file mode 100644
index 0000000..9a8cc0b
--- /dev/null
+++ b/test_result_interfaces/Android.bp
@@ -0,0 +1,27 @@
+// Copyright 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+ name: "tradefed-result-interfaces",
+ defaults: ["tradefed_defaults"],
+ srcs: [
+ "com/**/*.java",
+ ],
+ libs: [
+ "ddmlib-prebuilt",
+ "tradefed-common-util",
+ "tradefed-protos",
+ ],
+}
+
diff --git a/src/com/android/tradefed/log/ITestLogger.java b/test_result_interfaces/com/android/tradefed/log/ITestLogger.java
similarity index 96%
rename from src/com/android/tradefed/log/ITestLogger.java
rename to test_result_interfaces/com/android/tradefed/log/ITestLogger.java
index fbd29ef..d2d902d 100644
--- a/src/com/android/tradefed/log/ITestLogger.java
+++ b/test_result_interfaces/com/android/tradefed/log/ITestLogger.java
@@ -16,7 +16,6 @@
package com.android.tradefed.log;
-import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
diff --git a/src/com/android/tradefed/result/ITestLifeCycleReceiver.java b/test_result_interfaces/com/android/tradefed/result/ITestLifeCycleReceiver.java
similarity index 100%
rename from src/com/android/tradefed/result/ITestLifeCycleReceiver.java
rename to test_result_interfaces/com/android/tradefed/result/ITestLifeCycleReceiver.java
diff --git a/src/com/android/tradefed/result/ITestLoggerReceiver.java b/test_result_interfaces/com/android/tradefed/result/ITestLoggerReceiver.java
similarity index 100%
rename from src/com/android/tradefed/result/ITestLoggerReceiver.java
rename to test_result_interfaces/com/android/tradefed/result/ITestLoggerReceiver.java
diff --git a/src/com/android/tradefed/result/TestDescription.java b/test_result_interfaces/com/android/tradefed/result/TestDescription.java
similarity index 100%
rename from src/com/android/tradefed/result/TestDescription.java
rename to test_result_interfaces/com/android/tradefed/result/TestDescription.java
diff --git a/src/com/android/tradefed/util/proto/TestRecordProtoUtil.java b/test_result_interfaces/com/android/tradefed/util/proto/TestRecordProtoUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/proto/TestRecordProtoUtil.java
rename to test_result_interfaces/com/android/tradefed/util/proto/TestRecordProtoUtil.java
diff --git a/src/com/android/tradefed/util/proto/TfMetricProtoUtil.java b/test_result_interfaces/com/android/tradefed/util/proto/TfMetricProtoUtil.java
similarity index 100%
rename from src/com/android/tradefed/util/proto/TfMetricProtoUtil.java
rename to test_result_interfaces/com/android/tradefed/util/proto/TfMetricProtoUtil.java
diff --git a/tests/OWNERS b/tests/OWNERS
index f53b0d8..d24ee00 100644
--- a/tests/OWNERS
+++ b/tests/OWNERS
@@ -1,4 +1,3 @@
-# tests ownership is wider: base OWNERS + folks familiar with testing
-allenhair@google.com
-gelanchezhian@google.com
-mrosenfeld@google.com
+# tests ownership is wider for Tradefed Unit tests
+
+per-file *.java, *.xml = *
diff --git a/tests/res/config/suite/stub-parameterized.xml b/tests/res/config/suite/stub-parameterized.xml
index 78fab09..5925e14 100644
--- a/tests/res/config/suite/stub-parameterized.xml
+++ b/tests/res/config/suite/stub-parameterized.xml
@@ -15,6 +15,7 @@
-->
<configuration description="A stub module with a parameterized metadata">
<option name="metadata" key="parameter" value="instant_app" />
+ <option name="metadata" key="parameter" value="secondary_user" />
<option name="test-suite-tag" value="example-suite-parameters" />
<test class="com.android.tradefed.testtype.suite.TestSuiteStub" />
diff --git a/tests/res/testdata/test_mapping_golden1 b/tests/res/testdata/test_mapping_golden1
new file mode 100644
index 0000000..db3998d
--- /dev/null
+++ b/tests/res/testdata/test_mapping_golden1
@@ -0,0 +1,14 @@
+{
+ "presubmit": [
+ {
+ "name": "test1",
+ "host": true,
+ "include-filter": "testClass#testMethod"
+ }
+ ],
+ "imports": [
+ {
+ "path": "path1//path2//path3"
+ }
+ ]
+}
diff --git a/tests/res/testdata/test_mapping_golden2 b/tests/res/testdata/test_mapping_golden2
new file mode 100644
index 0000000..07486c0
--- /dev/null
+++ b/tests/res/testdata/test_mapping_golden2
@@ -0,0 +1,48 @@
+{
+ "presubmit": [
+ {
+ "name": "test1",
+ "host": true
+ },
+ {
+ "name": "suite/stub1"
+ },
+ {
+ "name": "suite/stub2",
+ "keywords": ["key_1"]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "test2",
+ "options": [
+ {
+ "instrumentation-arg": "annotation=android.platform.test.annotations.Presubmit"
+ }
+ ]
+ },
+ {
+ "name": "instrument",
+ "options": [
+ {
+ "run-name": "some-name"
+ }
+ ]
+ }
+ ],
+ "othertype": [
+ {
+ "name": "test3",
+ "options": [
+ {
+ "just-an-option": ""
+ }
+ ]
+ }
+ ],
+ "imports": [
+ {
+ "path": "folder1//folder2//folder3"
+ }
+ ]
+}
diff --git a/tests/res/testdata/test_mapping_with_comments1 b/tests/res/testdata/test_mapping_with_comments1
new file mode 100644
index 0000000..3f4083f
--- /dev/null
+++ b/tests/res/testdata/test_mapping_with_comments1
@@ -0,0 +1,16 @@
+{#comments1
+ "presubmit": [//comments2 // comments3 # comment4
+ #comments3
+ { #comments4
+ "name": "test1",#comments5
+//comments6
+ "host": true,//comments7
+ "include-filter": "testClass#testMethod" #comment11 // another comments
+ }#comments8
+ ],#comments9 // another comments
+ "imports": [
+ {
+ "path": "path1//path2//path3"#comment12
+ }
+ ]
+}#comments10
diff --git a/tests/res/testdata/test_mapping_with_comments2 b/tests/res/testdata/test_mapping_with_comments2
new file mode 100644
index 0000000..da5a503
--- /dev/null
+++ b/tests/res/testdata/test_mapping_with_comments2
@@ -0,0 +1,49 @@
+{//comment..// another comment # another comment
+ "presubmit": [ #comment //another comment #// another comment
+ { # comment
+ "name": "test1",//comment
+ "host": true#comment
+ },#comment
+ {
+ "name": "suite/stub1" // comment # comment
+ },
+ {
+ "name": "suite/stub2",
+ "keywords": ["key_1"] # comment
+ } # comment
+ ], // comment
+ "postsubmit": [
+ {
+ "name": "test2",
+ "options": [
+ {
+ "instrumentation-arg": "annotation=android.platform.test.annotations.Presubmit" //comment
+ }
+ ]
+ },
+ {
+ "name": "instrument",
+ "options": [
+ {
+ "run-name": "some-name"
+ }
+ ]
+ }
+ ],
+ "othertype": [ // another comment
+ {
+ "name": "test3",
+ "options": [
+ {
+ "just-an-option": ""##comment
+ }////another comment
+ //// another comment...
+ ]
+ }
+ ],
+ "imports": [
+ {
+ "path": "folder1//folder2//folder3"//comment... # another comment // another comment
+ }
+ ]
+}
diff --git a/tests/res/util/partial_zip.zip b/tests/res/util/partial_zip.zip
new file mode 100644
index 0000000..1f50fef
--- /dev/null
+++ b/tests/res/util/partial_zip.zip
Binary files differ
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index c84ef69..1f5647d 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -26,6 +26,7 @@
import com.android.tradefed.build.GCSTestResourceProviderTest;
import com.android.tradefed.build.LocalDeviceBuildProviderTest;
import com.android.tradefed.build.OtaZipfileBuildProviderTest;
+import com.android.tradefed.clearcut.ClearcutClientTest;
import com.android.tradefed.command.CommandFileParserTest;
import com.android.tradefed.command.CommandFileWatcherTest;
import com.android.tradefed.command.CommandInterrupterTest;
@@ -33,7 +34,6 @@
import com.android.tradefed.command.CommandRunnerTest;
import com.android.tradefed.command.CommandSchedulerTest;
import com.android.tradefed.command.ConsoleTest;
-import com.android.tradefed.command.VerifyTest;
import com.android.tradefed.command.remote.RemoteManagerTest;
import com.android.tradefed.command.remote.RemoteOperationTest;
import com.android.tradefed.config.ArgsOptionParserTest;
@@ -52,22 +52,20 @@
import com.android.tradefed.config.gcs.GCSConfigurationFactoryTest;
import com.android.tradefed.config.gcs.GCSConfigurationServerTest;
import com.android.tradefed.config.remote.GcsRemoteFileResolverTest;
+import com.android.tradefed.config.remote.HttpRemoteFileResolverTest;
+import com.android.tradefed.config.remote.LocalFileResolverTest;
import com.android.tradefed.device.AndroidDebugBridgeWrapperTest;
import com.android.tradefed.device.BackgroundDeviceActionTest;
-import com.android.tradefed.device.CpuStatsCollectorTest;
import com.android.tradefed.device.DeviceManagerTest;
import com.android.tradefed.device.DeviceSelectionOptionsTest;
import com.android.tradefed.device.DeviceStateMonitorTest;
-import com.android.tradefed.device.DeviceUtilStatsMonitorTest;
import com.android.tradefed.device.DumpsysPackageReceiverTest;
import com.android.tradefed.device.FastbootHelperTest;
import com.android.tradefed.device.ManagedDeviceListTest;
import com.android.tradefed.device.ManagedTestDeviceFactoryTest;
import com.android.tradefed.device.NativeDeviceTest;
-import com.android.tradefed.device.ReconnectingRecoveryTest;
import com.android.tradefed.device.RemoteAndroidDeviceTest;
import com.android.tradefed.device.TestDeviceTest;
-import com.android.tradefed.device.TopHelperTest;
import com.android.tradefed.device.WaitDeviceRecoveryTest;
import com.android.tradefed.device.WifiHelperTest;
import com.android.tradefed.device.cloud.AcloudConfigParserTest;
@@ -75,6 +73,7 @@
import com.android.tradefed.device.cloud.GceManagerTest;
import com.android.tradefed.device.cloud.GceRemoteCmdFormatterTest;
import com.android.tradefed.device.cloud.GceSshTunnelMonitorTest;
+import com.android.tradefed.device.cloud.ManagedRemoteDeviceTest;
import com.android.tradefed.device.cloud.NestedRemoteDeviceTest;
import com.android.tradefed.device.cloud.RemoteFileUtilTest;
import com.android.tradefed.device.contentprovider.ContentProviderHandlerTest;
@@ -121,6 +120,7 @@
import com.android.tradefed.invoker.TestInvocationMultiTest;
import com.android.tradefed.invoker.TestInvocationTest;
import com.android.tradefed.invoker.UnexecutedTestReporterThreadTest;
+import com.android.tradefed.invoker.logger.InvocationMetricLoggerTest;
import com.android.tradefed.invoker.monitor.InvocationsMonitorTest;
import com.android.tradefed.invoker.sandbox.ParentSandboxInvocationExecutionTest;
import com.android.tradefed.invoker.shard.ShardHelperTest;
@@ -130,6 +130,7 @@
import com.android.tradefed.log.FileLoggerTest;
import com.android.tradefed.log.HistoryLoggerTest;
import com.android.tradefed.log.LogRegistryTest;
+import com.android.tradefed.log.SimpleFileLoggerTest;
import com.android.tradefed.log.TerribleFailureEmailHandlerTest;
import com.android.tradefed.postprocessor.AggregatePostProcessorTest;
import com.android.tradefed.postprocessor.AveragePostProcessorTest;
@@ -158,6 +159,7 @@
import com.android.tradefed.result.TestRunResultTest;
import com.android.tradefed.result.TestSummaryTest;
import com.android.tradefed.result.XmlResultReporterTest;
+import com.android.tradefed.result.ddmlib.InstrumentationResultProtoParserTest;
import com.android.tradefed.result.ddmlib.TestRunToTestInvocationForwarderTest;
import com.android.tradefed.result.proto.FileProtoResultReporterTest;
import com.android.tradefed.result.proto.ProtoResultParserTest;
@@ -213,7 +215,9 @@
import com.android.tradefed.targetprep.UserCleanerTest;
import com.android.tradefed.targetprep.adb.AdbStopServerPreparerTest;
import com.android.tradefed.targetprep.app.NoApkTestSkipperTest;
+import com.android.tradefed.targetprep.multi.DynamicSystemPreparerTest;
import com.android.tradefed.targetprep.multi.MergeMultiBuildTargetPreparerTest;
+import com.android.tradefed.targetprep.multi.MixImageZipPreparerTest;
import com.android.tradefed.targetprep.suite.SuiteApkInstallerTest;
import com.android.tradefed.testtype.AndroidJUnitTestTest;
import com.android.tradefed.testtype.DeviceBatteryLevelCheckerTest;
@@ -245,13 +249,13 @@
import com.android.tradefed.testtype.PythonUnitTestResultParserTest;
import com.android.tradefed.testtype.PythonUnitTestRunnerTest;
import com.android.tradefed.testtype.TfTestLauncherTest;
-import com.android.tradefed.testtype.VersionedTfLauncherTest;
import com.android.tradefed.testtype.binary.ExecutableHostTestTest;
import com.android.tradefed.testtype.host.CoverageMeasurementForwarderTest;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4TestTest;
import com.android.tradefed.testtype.junit4.DeviceParameterizedRunnerTest;
import com.android.tradefed.testtype.junit4.LongevityHostRunnerTest;
import com.android.tradefed.testtype.python.PythonBinaryHostTestTest;
+import com.android.tradefed.testtype.retry.ResultAggregatorTest;
import com.android.tradefed.testtype.suite.AtestRunnerTest;
import com.android.tradefed.testtype.suite.BaseTestSuiteTest;
import com.android.tradefed.testtype.suite.GranularRetriableTestWrapperTest;
@@ -276,8 +280,6 @@
import com.android.tradefed.testtype.suite.params.ModuleParametersHelperTest;
import com.android.tradefed.testtype.suite.retry.ResultsPlayerTest;
import com.android.tradefed.testtype.suite.retry.RetryReschedulerTest;
-import com.android.tradefed.testtype.testdefs.XmlDefsParserTest;
-import com.android.tradefed.testtype.testdefs.XmlDefsTestTest;
import com.android.tradefed.util.AaptParserTest;
import com.android.tradefed.util.AbiFormatterTest;
import com.android.tradefed.util.AbiUtilsTest;
@@ -306,6 +308,7 @@
import com.android.tradefed.util.LocalRunInstructionBuilderTest;
import com.android.tradefed.util.LogcatEventParserTest;
import com.android.tradefed.util.MultiMapTest;
+import com.android.tradefed.util.NativeCodeCoverageFlusherTest;
import com.android.tradefed.util.NullUtilTest;
import com.android.tradefed.util.PairTest;
import com.android.tradefed.util.PropertyChangerTest;
@@ -329,7 +332,6 @@
import com.android.tradefed.util.TestLoaderTest;
import com.android.tradefed.util.TimeUtilTest;
import com.android.tradefed.util.TimeValTest;
-import com.android.tradefed.util.UserUtilTest;
import com.android.tradefed.util.VersionParserTest;
import com.android.tradefed.util.ZipUtil2Test;
import com.android.tradefed.util.ZipUtilTest;
@@ -348,7 +350,7 @@
import com.android.tradefed.util.statsd.MetricUtilTest;
import com.android.tradefed.util.testmapping.TestInfoTest;
import com.android.tradefed.util.testmapping.TestMappingTest;
-import com.android.tradefed.util.xml.AndroidManifestWriterTest;
+import com.android.tradefed.util.zip.MergedZipEntryCollectionTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@@ -375,6 +377,9 @@
LocalDeviceBuildProviderTest.class,
OtaZipfileBuildProviderTest.class,
+ // clearcut
+ ClearcutClientTest.class,
+
// command
CommandFileParserTest.class,
CommandFileWatcherTest.class,
@@ -383,7 +388,6 @@
CommandRunnerTest.class,
CommandSchedulerTest.class,
ConsoleTest.class,
- VerifyTest.class,
// command.remote
RemoteManagerTest.class,
@@ -410,25 +414,23 @@
// config.remote
GcsRemoteFileResolverTest.class,
+ HttpRemoteFileResolverTest.class,
+ LocalFileResolverTest.class,
// device
AndroidDebugBridgeWrapperTest.class,
BackgroundDeviceActionTest.class,
- CpuStatsCollectorTest.class,
DeviceManagerTest.class,
DeviceSelectionOptionsTest.class,
DeviceStateMonitorTest.class,
- DeviceUtilStatsMonitorTest.class,
DumpsysPackageReceiverTest.class,
FastbootHelperTest.class,
ManagedDeviceListTest.class,
ManagedTestDeviceFactoryTest.class,
NativeDeviceTest.class,
- ReconnectingRecoveryTest.class,
RemoteAndroidDeviceTest.class,
PropertyChangerTest.class,
TestDeviceTest.class,
- TopHelperTest.class,
WaitDeviceRecoveryTest.class,
WifiHelperTest.class,
@@ -438,6 +440,7 @@
GceManagerTest.class,
GceRemoteCmdFormatterTest.class,
GceSshTunnelMonitorTest.class,
+ ManagedRemoteDeviceTest.class,
NestedRemoteDeviceTest.class,
RemoteAndroidDeviceTest.class,
RemoteFileUtilTest.class,
@@ -502,6 +505,9 @@
TestInvocationTest.class,
UnexecutedTestReporterThreadTest.class,
+ // invoker.logger
+ InvocationMetricLoggerTest.class,
+
// invoker.monitor
InvocationsMonitorTest.class,
@@ -520,6 +526,7 @@
FileLoggerTest.class,
HistoryLoggerTest.class,
LogRegistryTest.class,
+ SimpleFileLoggerTest.class,
TerribleFailureEmailHandlerTest.class,
// postprocessor
@@ -554,6 +561,7 @@
XmlResultReporterTest.class,
// result.ddmlib
+ InstrumentationResultProtoParserTest.class,
TestRunToTestInvocationForwarderTest.class,
// result.proto
@@ -578,6 +586,7 @@
DeviceStorageFillerTest.class,
DeviceStringPusherTest.class,
DisableSELinuxTargetPreparerTest.class,
+ DynamicSystemPreparerTest.class,
FastbootDeviceFlasherTest.class,
FlashingResourcesParserTest.class,
InstallAllTestZipAppsSetupTest.class,
@@ -608,6 +617,7 @@
// targetprep.multi
MergeMultiBuildTargetPreparerTest.class,
+ MixImageZipPreparerTest.class,
// targetprep.suite
SuiteApkInstallerTest.class,
@@ -663,7 +673,6 @@
PythonUnitTestResultParserTest.class,
PythonUnitTestRunnerTest.class,
TfTestLauncherTest.class,
- VersionedTfLauncherTest.class,
// testtype/binary
ExecutableHostTestTest.class,
@@ -676,6 +685,9 @@
// testtype/python
PythonBinaryHostTestTest.class,
+ // testtype/retry
+ ResultAggregatorTest.class,
+
// testtype/suite
AtestRunnerTest.class,
BaseTestSuiteTest.class,
@@ -708,10 +720,6 @@
ResultsPlayerTest.class,
RetryReschedulerTest.class,
- // testtype/testdefs
- XmlDefsParserTest.class,
- XmlDefsTestTest.class,
-
// util
AaptParserTest.class,
AbiFormatterTest.class,
@@ -741,6 +749,8 @@
ListInstrumentationParserTest.class,
LogcatEventParserTest.class,
MultiMapTest.class,
+ MergedZipEntryCollectionTest.class,
+ NativeCodeCoverageFlusherTest.class,
NullUtilTest.class,
PairTest.class,
PsParserTest.class,
@@ -763,7 +773,6 @@
TestLoaderTest.class,
TimeUtilTest.class,
TimeValTest.class,
- UserUtilTest.class,
VersionParserTest.class,
ZipUtilTest.class,
ZipUtil2Test.class,
@@ -796,9 +805,6 @@
// util/testmapping
TestInfoTest.class,
TestMappingTest.class,
-
- // util/xml
- AndroidManifestWriterTest.class,
})
public class UnitTests {
// empty of purpose
diff --git a/tests/src/com/android/tradefed/build/BuildInfoTest.java b/tests/src/com/android/tradefed/build/BuildInfoTest.java
index c54291e..46d4bd0 100644
--- a/tests/src/com/android/tradefed/build/BuildInfoTest.java
+++ b/tests/src/com/android/tradefed/build/BuildInfoTest.java
@@ -184,11 +184,20 @@
/** Test that the build info can be described in its proto format. */
@Test
public void testProtoSerialization() throws Exception {
+ List<String> remoteFiles = Arrays.asList("remote/file1", "remote/file2");
+ for (String file : remoteFiles) {
+ mBuildInfo.setFile(
+ IBuildInfo.REMOTE_FILE_PREFIX + file,
+ new File(file),
+ IBuildInfo.REMOTE_FILE_VERSION);
+ }
+
BuildInformation.BuildInfo proto = mBuildInfo.toProto();
+
assertEquals("1", proto.getBuildId());
assertEquals(BuildInfo.class.getCanonicalName(), proto.getBuildInfoClass());
assertEquals("value", proto.getAttributes().get("attribute"));
- assertEquals(1, proto.getVersionedFileList().size());
+ assertEquals(3, proto.getVersionedFileList().size());
assertNotNull(proto.getVersionedFileList().get(0));
IBuildInfo deserialized = BuildInfo.fromProto(proto);
@@ -196,6 +205,11 @@
// Build flavor was not set, so it's null
assertNull(deserialized.getBuildFlavor());
assertNotNull(deserialized.getVersionedFile(FILE_KEY));
+
+ // Check the remote files are restored.
+ for (String file : remoteFiles) {
+ assertTrue(deserialized.getRemoteFiles().contains(new File(file)));
+ }
}
/** Test {@link BuildInfo#getTestResource(List, String)} */
diff --git a/tests/src/com/android/tradefed/clearcut/ClearcutClientTest.java b/tests/src/com/android/tradefed/clearcut/ClearcutClientTest.java
new file mode 100644
index 0000000..fcab272
--- /dev/null
+++ b/tests/src/com/android/tradefed/clearcut/ClearcutClientTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.clearcut;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+/** Unit tests for {@link ClearcutClient}. */
+@RunWith(JUnit4.class)
+public class ClearcutClientTest {
+
+ private ClearcutClient mClient;
+
+ @Before
+ public void setUp() {
+ mClient =
+ new ClearcutClient("url") {
+ @Override
+ boolean isGoogleUser() {
+ return false;
+ }
+ };
+ }
+
+ @After
+ public void tearDown() {
+ mClient.stop();
+ }
+
+ @Test
+ public void testGetGroupingKey() throws Exception {
+ File testFile = FileUtil.createTempFile("uuid-test", "");
+ try {
+ mClient.setCachedUuidFile(testFile);
+ String grouping = mClient.getGroupingKey();
+ // Key was created and written to cached file.
+ assertEquals(grouping, FileUtil.readStringFromFile(testFile));
+ } finally {
+ FileUtil.deleteFile(testFile);
+ }
+ }
+
+ @Test
+ public void testGetGroupingKey_exists() throws Exception {
+ File testFile = FileUtil.createTempFile("uuid-test", "");
+ try {
+ FileUtil.writeToFile("test", testFile);
+ mClient.setCachedUuidFile(testFile);
+ String grouping = mClient.getGroupingKey();
+ assertEquals("test", grouping);
+ } finally {
+ FileUtil.deleteFile(testFile);
+ }
+ }
+
+ @Test
+ public void testDisableClient() {
+ ClearcutClient c =
+ new ClearcutClient("url") {
+ @Override
+ boolean isClearcutDisabled() {
+ return true;
+ }
+
+ @Override
+ boolean isGoogleUser() {
+ throw new RuntimeException("Should not be called if disabled");
+ }
+ };
+ try {
+ c.notifyTradefedStartEvent();
+ c.notifyTradefedStartEvent();
+ c.notifyTradefedStartEvent();
+ assertEquals(0, c.getQueueSize());
+ } finally {
+ c.stop();
+ }
+ }
+}
diff --git a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
index 8e51cb0..8c9e9f4 100644
--- a/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
+++ b/tests/src/com/android/tradefed/command/CommandSchedulerTest.java
@@ -34,7 +34,6 @@
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.IConfigurationFactory;
import com.android.tradefed.config.IDeviceConfiguration;
-import com.android.tradefed.config.IGlobalConfiguration;
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceSelectionOptions;
@@ -52,8 +51,6 @@
import com.android.tradefed.invoker.ITestInvocation;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.log.ILogRegistry.EventType;
-import com.android.tradefed.log.ITerribleFailureHandler;
-import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.RunUtil;
@@ -301,8 +298,8 @@
String[] args2 = new String[] {"test"};
setCreateConfigExpectations(args2, 1);
setExpectedInvokeCalls(1);
- mMockConfiguration.validateOptions(false);
mMockConfiguration.validateOptions();
+ EasyMock.expectLastCall().times(2);
replayMocks();
mScheduler.start();
@@ -465,40 +462,6 @@
}
}
- /** Verify that scheduler goes into shutdown mode when a {@link FatalHostError} is thrown. */
- @Test
- public void testRun_fatalError() throws Throwable {
- mMockInvocation.invoke((IInvocationContext)EasyMock.anyObject(),
- (IConfiguration)EasyMock.anyObject(), (IRescheduler)EasyMock.anyObject(),
- (ITestInvocationListener)EasyMock.anyObject());
- EasyMock.expectLastCall().andThrow(new FatalHostError("error"));
- // set up a mock global config and wtfhandler to handle CLog.wtf when FatalHostError occurs
- IGlobalConfiguration mockGc = EasyMock.createMock(IGlobalConfiguration.class);
- CLog.setGlobalConfigInstance(mockGc);
- try {
- ITerribleFailureHandler mockWtf = EasyMock.createMock(ITerribleFailureHandler.class);
- EasyMock.expect(mockGc.getWtfHandler()).andReturn(mockWtf).anyTimes();
- EasyMock.expect(mockWtf.onTerribleFailure((String)EasyMock.anyObject(),
- (Throwable)EasyMock.anyObject())).andReturn(Boolean.TRUE);
- String[] args = new String[] {"test"};
- mMockManager.setNumDevices(2);
- setCreateConfigExpectations(args, 1);
- mMockConfiguration.validateOptions();
- replayMocks(mockGc, mockWtf);
- mScheduler.start();
- mScheduler.addCommand(args);
- // no need to call shutdown explicitly - scheduler should shutdown by itself
- mScheduler.join(2 * 1000);
- // We don't verify the mockManager for this test since after failure, the device might
- // not have time to go back to list before shutdown on scheduler.
- EasyMock.verify(
- mMockConfigFactory, mMockConfiguration, mMockInvocation, mockGc, mockWtf);
- } finally {
- // reset global config to null, which means 'not overloaded/use default'
- CLog.setGlobalConfigInstance(null);
- }
- }
-
/**
* Test{@link CommandScheduler#run()} when config is matched to a specific device serial number
*
diff --git a/tests/src/com/android/tradefed/command/VerifyTest.java b/tests/src/com/android/tradefed/command/VerifyTest.java
deleted file mode 100644
index e92b5d4..0000000
--- a/tests/src/com/android/tradefed/command/VerifyTest.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.command;
-
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.util.FileUtil;
-
-import junit.framework.TestCase;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Unit tests for {@link Verify}
- */
-public class VerifyTest extends TestCase {
- private static final String TEST_CMD_FILE_PATH = "/testCmdFiles";
- private final Verify mVerify;
-
- public VerifyTest() throws ConfigurationException {
- mVerify = new Verify();
-
- OptionSetter option = new OptionSetter(mVerify);
- option.setOptionValue("quiet", "true");
- }
-
- /**
- * Extract an embedded command file into a temporary file, which we can feed to the
- * CommandFileParser
- */
- private File extractTestCmdFile(String name) throws IOException {
- final InputStream cmdFileStream = getClass().getResourceAsStream(
- String.format("%s/%s.txt", TEST_CMD_FILE_PATH, name));
- final String tmpFileName = String.format("VerifyTest_%s_", name);
- File tmpFile = FileUtil.createTempFile(tmpFileName, ".txt");
- try {
- FileUtil.writeToFile(cmdFileStream, tmpFile);
- } catch (Throwable t) {
- // Clean up tmpFile, if it was created.
- FileUtil.deleteFile(tmpFile);
- throw t;
- }
-
- return tmpFile;
- }
-
- /**
- * Assert that the specified command file parses correctly, and clean up any temporary files
- */
- private void assertGoodCmdFile(String name) throws IOException {
- File cmdFile = extractTestCmdFile(name);
- try {
- assertTrue(mVerify.runVerify(cmdFile));
- } finally {
- FileUtil.deleteFile(cmdFile);
- }
- }
-
- /**
- * Assert that the specified command file does not parse correctly, and clean up any temporary
- * files
- */
- private void assertBadCmdFile(String name) throws IOException {
- File cmdFile = extractTestCmdFile(name);
- try {
- assertFalse(mVerify.runVerify(cmdFile));
- } finally {
- FileUtil.deleteFile(cmdFile);
- }
- }
-
- public void testBasic() throws IOException {
- assertGoodCmdFile("basic");
- }
-
- public void testMissingMacroDef() throws IOException {
- assertBadCmdFile("missing-macro-def");
- }
-
- public void testMissingBeginMacro() throws IOException {
- assertBadCmdFile("missing-begin-macro");
- }
-
- public void testMissingEndMacro() throws IOException {
- assertBadCmdFile("missing-end-macro");
- }
-}
diff --git a/tests/src/com/android/tradefed/config/ConfigurationTest.java b/tests/src/com/android/tradefed/config/ConfigurationTest.java
index ed189a5..5649fb0 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationTest.java
@@ -20,7 +20,6 @@
import com.android.tradefed.build.IBuildProvider;
import com.android.tradefed.command.CommandOptions;
import com.android.tradefed.command.ICommandOptions;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IDeviceRecovery;
import com.android.tradefed.device.IDeviceSelection;
@@ -32,6 +31,7 @@
import com.android.tradefed.targetprep.ITargetPreparer;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IDisableable;
import com.android.tradefed.util.MultiMap;
import junit.framework.TestCase;
@@ -71,7 +71,7 @@
public boolean getBool();
}
- private static class TestConfigObject implements TestConfig {
+ private static class TestConfigObject implements TestConfig, IDisableable {
@Option(name = OPTION_NAME, description = OPTION_DESCRIPTION, requiredForRerun = true)
private boolean mBool;
@@ -79,6 +79,11 @@
@Option(name = ALT_OPTION_NAME, description = OPTION_DESCRIPTION)
private Map<String, Boolean> mBoolMap = new HashMap<String, Boolean>();
+ @Option(name = "mandatory-option", mandatory = true)
+ private String mMandatory = null;
+
+ private boolean mIsDisabled = false;
+
@Override
public boolean getBool() {
return mBool;
@@ -87,6 +92,16 @@
public Map<String, Boolean> getMap() {
return mBoolMap;
}
+
+ @Override
+ public void setDisable(boolean isDisabled) {
+ mIsDisabled = isDisabled;
+ }
+
+ @Override
+ public boolean isDisabled() {
+ return mIsDisabled;
+ }
}
private Configuration mConfig;
@@ -98,6 +113,11 @@
protected void setUp() throws Exception {
super.setUp();
mConfig = new Configuration(CONFIG_NAME, CONFIG_DESCRIPTION);
+
+ try {
+ GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
+ } catch (IllegalStateException ignored) {
+ }
}
/**
@@ -570,6 +590,33 @@
}
/**
+ * Test that {@link Configuration#validateOptions()} throw when all mandatory fields are not set
+ * and object is not disabled.
+ */
+ public void testValidateOptions_nonDisabledObject() throws ConfigurationException {
+ TestConfigObject object = new TestConfigObject();
+ object.setDisable(false);
+ mConfig.setConfigurationObject("helper", object);
+ try {
+ mConfig.validateOptions();
+ fail("Should have thrown an exception.");
+ } catch (ConfigurationException expected) {
+ assertTrue(expected.getMessage().contains("Found missing mandatory options"));
+ }
+ }
+
+ /**
+ * Test that {@link Configuration#validateOptions()} doesn't throw when all mandatory fields are
+ * not set but the object is disabled.
+ */
+ public void testValidateOptions_disabledObject() throws ConfigurationException {
+ TestConfigObject object = new TestConfigObject();
+ object.setDisable(true);
+ mConfig.setConfigurationObject("helper", object);
+ mConfig.validateOptions();
+ }
+
+ /**
* Test that {@link Configuration#validateOptions()} throws a config exception when shard
* count is negative number.
*/
@@ -647,7 +694,8 @@
mConfig.setDeviceOptions(deviceOptions);
// No exception for download is thrown because no download occurred.
- mConfig.validateOptions(true);
+ mConfig.validateOptions();
+ mConfig.resolveDynamicOptions();
// Dynamic file is not resolved.
assertEquals(fakeConfigFile, deviceOptions.getAvdConfigFile());
}
diff --git a/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java b/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
index 75535e4..bd4fb49 100644
--- a/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
+++ b/tests/src/com/android/tradefed/config/ConfigurationUtilTest.java
@@ -47,8 +47,8 @@
private static final String DEVICE_MANAGER_TYPE_NAME = "device_manager";
/**
- * Test {@link ConfigurationUtil#dumpClassToXml(KXmlSerializer, String, Object, List, boolean)}
- * to create a dump of a configuration.
+ * Test {@link ConfigurationUtil#dumpClassToXml(KXmlSerializer, String, Object, List, boolean,
+ * boolean)} to create a dump of a configuration.
*/
@Test
public void testDumpClassToXml() throws Throwable {
@@ -67,6 +67,7 @@
DEVICE_MANAGER_TYPE_NAME,
deviceManager,
new ArrayList<String>(),
+ true,
true);
serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
@@ -87,8 +88,8 @@
}
/**
- * Test {@link ConfigurationUtil#dumpClassToXml(KXmlSerializer, String, Object, List, boolean)}
- * to create a dump of a configuration with filters
+ * Test {@link ConfigurationUtil#dumpClassToXml(KXmlSerializer, String, Object, List, boolean,
+ * boolean)} to create a dump of a configuration with filters
*/
@Test
public void testDumpClassToXml_filtered() throws Throwable {
@@ -107,12 +108,14 @@
GlobalConfiguration.DEVICE_MANAGER_TYPE_NAME,
deviceManager,
Arrays.asList("com.android.tradefed.device.DeviceManager"),
+ true,
true);
ConfigurationUtil.dumpClassToXml(
serializer,
GlobalConfiguration.SCHEDULER_TYPE_NAME,
new CommandScheduler(),
Arrays.asList("com.android.tradefed.device.DeviceManager"),
+ true,
true);
serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
@@ -284,7 +287,8 @@
Configuration.TARGET_PREPARER_TYPE_NAME,
preparer,
new ArrayList<String>(),
- false);
+ false,
+ true);
serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
serializer.endDocument();
@@ -304,4 +308,46 @@
FileUtil.deleteFile(tmpXml);
}
}
+
+ /** Only print options that have been changed. */
+ @Test
+ public void testDumpClassToXml_filterNotChanged() throws Throwable {
+ File tmpXml = FileUtil.createTempFile("global_config", ".xml");
+ try {
+ PrintWriter output = new PrintWriter(tmpXml);
+ KXmlSerializer serializer = new KXmlSerializer();
+ serializer.setOutput(output);
+ serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ serializer.startDocument("UTF-8", null);
+ serializer.startTag(null, ConfigurationUtil.CONFIGURATION_NAME);
+
+ ITargetPreparer preparer = new TestTargetPreparer();
+ OptionSetter changeOneOption = new OptionSetter(preparer);
+ changeOneOption.setOptionValue("real-option", "true");
+ ConfigurationUtil.dumpClassToXml(
+ serializer,
+ Configuration.TARGET_PREPARER_TYPE_NAME,
+ preparer,
+ new ArrayList<String>(),
+ true,
+ false);
+
+ serializer.endTag(null, ConfigurationUtil.CONFIGURATION_NAME);
+ serializer.endDocument();
+
+ // Read the dump XML file, make sure configurations can be loaded.
+ String content = FileUtil.readStringFromFile(tmpXml);
+ assertTrue(content.length() > 100);
+ assertTrue(content.contains("<configuration>"));
+ assertTrue(content.contains("<option name=\"real-option\" value=\"true\" />"));
+ // Does not contain any trace of the deprecated option
+ assertFalse(content.contains("deprecated-option"));
+ assertTrue(
+ content.contains(
+ "<target_preparer class=\"com.android.tradefed.config."
+ + "ConfigurationUtilTest$TestTargetPreparer\">"));
+ } finally {
+ FileUtil.deleteFile(tmpXml);
+ }
+ }
}
diff --git a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
index 9958fb1..fc1c130 100644
--- a/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/DynamicRemoteFileResolverTest.java
@@ -16,6 +16,7 @@
package com.android.tradefed.config;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -32,9 +33,11 @@
import java.io.File;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -44,6 +47,7 @@
@RunWith(JUnit4.class)
public class DynamicRemoteFileResolverTest {
+ @OptionClass(alias = "alias-remote-file")
private static class RemoteFileOption {
@Option(name = "remote-file")
public File remoteFile = null;
@@ -74,7 +78,7 @@
new DynamicRemoteFileResolver() {
@Override
protected IRemoteFileResolver getResolver(String protocol) {
- if (protocol.equals(GcsRemoteFileResolver.PROTOCOL)) {
+ if (GcsRemoteFileResolver.PROTOCOL.equals(protocol)) {
return mMockResolver;
}
return null;
@@ -106,7 +110,9 @@
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andReturn(fake);
EasyMock.replay(mMockResolver);
@@ -125,6 +131,82 @@
}
@Test
+ public void testResolveWithQuery() throws Exception {
+ RemoteFileOption object = new RemoteFileOption();
+ OptionSetter setter =
+ new OptionSetter(object) {
+ @Override
+ protected DynamicRemoteFileResolver createResolver() {
+ return mResolver;
+ }
+ };
+
+ File fake = FileUtil.createTempFile("gs-option-setter-test", "txt");
+
+ setter.setOptionValue("remote-file", "gs://fake/path?key=value");
+ assertEquals("gs:/fake/path?key=value", object.remoteFile.getPath());
+
+ Map<String, String> testMap = new HashMap<>();
+ testMap.put("key", "value");
+ EasyMock.expect(
+ mMockResolver.resolveRemoteFiles(
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.anyObject(),
+ EasyMock.eq(testMap)))
+ .andReturn(fake);
+ EasyMock.replay(mMockResolver);
+
+ Set<File> downloadedFile = setter.validateRemoteFilePath();
+ try {
+ assertEquals(1, downloadedFile.size());
+ File downloaded = downloadedFile.iterator().next();
+ // The file has been replaced by the downloaded one.
+ assertEquals(downloaded.getAbsolutePath(), object.remoteFile.getAbsolutePath());
+ } finally {
+ for (File f : downloadedFile) {
+ FileUtil.recursiveDelete(f);
+ }
+ }
+ EasyMock.verify(mMockResolver);
+ }
+
+ /** Test to make sure that a dynamic download marked as "optional" does not throw */
+ @Test
+ public void testResolveOptional() throws Exception {
+ RemoteFileOption object = new RemoteFileOption();
+ OptionSetter setter =
+ new OptionSetter(object) {
+ @Override
+ protected DynamicRemoteFileResolver createResolver() {
+ return mResolver;
+ }
+ };
+
+ setter.setOptionValue("remote-file", "gs://fake/path?optional=true");
+ assertEquals("gs:/fake/path?optional=true", object.remoteFile.getPath());
+
+ Map<String, String> testMap = new HashMap<>();
+ testMap.put("optional", "true");
+ EasyMock.expect(
+ mMockResolver.resolveRemoteFiles(
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.anyObject(),
+ EasyMock.eq(testMap)))
+ .andThrow(new ConfigurationException("Failed to download"));
+ EasyMock.replay(mMockResolver);
+
+ Set<File> downloadedFile = setter.validateRemoteFilePath();
+ try {
+ assertEquals(0, downloadedFile.size());
+ } finally {
+ for (File f : downloadedFile) {
+ FileUtil.recursiveDelete(f);
+ }
+ }
+ EasyMock.verify(mMockResolver);
+ }
+
+ @Test
public void testResolve_remoteFileList() throws Exception {
RemoteFileOption object = new RemoteFileOption();
OptionSetter setter =
@@ -143,7 +225,9 @@
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andReturn(fake);
EasyMock.replay(mMockResolver);
@@ -186,16 +270,20 @@
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
EasyMock.eq(new File("gs://success/fake/path")),
+ EasyMock.anyObject(),
EasyMock.anyObject()))
.andReturn(fake);
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
EasyMock.eq(new File("gs://success/fake/path2")),
+ EasyMock.anyObject(),
EasyMock.anyObject()))
.andReturn(fake);
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs://failure/test")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs://failure/test")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andThrow(new ConfigurationException("retrieval error"));
EasyMock.replay(mMockResolver);
try {
@@ -229,11 +317,15 @@
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andReturn(fake);
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path2")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs:/fake/path2")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andReturn(fake2);
EasyMock.replay(mMockResolver);
@@ -276,15 +368,21 @@
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andReturn(fake);
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path2")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs:/fake/path2")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andReturn(fake2);
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path3")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs:/fake/path3")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andReturn(fake3);
EasyMock.replay(mMockResolver);
@@ -325,7 +423,9 @@
// anymore
EasyMock.expect(
mMockResolver.resolveRemoteFiles(
- EasyMock.eq(new File("gs:/fake/path")), EasyMock.anyObject()))
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
.andReturn(fake);
EasyMock.replay(mMockResolver);
@@ -421,4 +521,102 @@
}
EasyMock.verify(mMockResolver);
}
+
+ @Test
+ public void testResolvePartialDownloadZip() throws Exception {
+ List<String> includeFilters = Arrays.asList("test1", "test2");
+ List<String> excludeFilters = Arrays.asList("[.]config");
+
+ Map<String, String> queryArgs = new HashMap<>();
+ queryArgs.put("partial_download_dir", "/tmp");
+ queryArgs.put("include_filters", "test1;test2");
+ queryArgs.put("exclude_filters", "[.]config");
+ EasyMock.expect(
+ mMockResolver.resolveRemoteFiles(
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.eq(null),
+ EasyMock.eq(queryArgs)))
+ .andReturn(null);
+ EasyMock.replay(mMockResolver);
+
+ mResolver.resolvePartialDownloadZip(
+ new File("/tmp"), "gs:/fake/path", includeFilters, excludeFilters);
+ EasyMock.verify(mMockResolver);
+ }
+
+ /** Ignore any error if the download request is optional. */
+ @Test
+ public void testResolvePartialDownloadZip_optional() throws Exception {
+ List<String> includeFilters = Arrays.asList("test1", "test2");
+ List<String> excludeFilters = Arrays.asList("[.]config");
+
+ Map<String, String> queryArgs = new HashMap<>();
+ queryArgs.put("partial_download_dir", "/tmp");
+ queryArgs.put("include_filters", "test1;test2");
+ queryArgs.put("exclude_filters", "[.]config");
+ queryArgs.put("optional", "true");
+ EasyMock.expect(
+ mMockResolver.resolveRemoteFiles(
+ EasyMock.eq(new File("gs:/fake/path?optional=true")),
+ EasyMock.eq(null),
+ EasyMock.eq(queryArgs)))
+ .andThrow(new ConfigurationException("should not throw this exception."));
+ EasyMock.replay(mMockResolver);
+
+ mResolver.resolvePartialDownloadZip(
+ new File("/tmp"), "gs:/fake/path?optional=true", includeFilters, excludeFilters);
+ EasyMock.verify(mMockResolver);
+ }
+
+ /**
+ * Ensure that the same field on two different objects can be set with different remote values.
+ */
+ @Test
+ public void testResolveTwoObjects() throws Exception {
+ RemoteFileOption object1 = new RemoteFileOption();
+ RemoteFileOption object2 = new RemoteFileOption();
+ OptionSetter setter =
+ new OptionSetter(object1, object2) {
+ @Override
+ protected DynamicRemoteFileResolver createResolver() {
+ return mResolver;
+ }
+ };
+
+ File fake = FileUtil.createTempFile("gs-option-setter-test", "txt");
+ setter.setOptionValue("alias-remote-file:1:remote-file", "gs://fake/path");
+ assertEquals("gs:/fake/path", object1.remoteFile.getPath());
+
+ File fake2 = FileUtil.createTempFile("gs-option-setter-test", "txt");
+ setter.setOptionValue("alias-remote-file:2:remote-file", "gs://fake2/path2");
+ assertEquals("gs:/fake2/path2", object2.remoteFile.getPath());
+
+ EasyMock.expect(
+ mMockResolver.resolveRemoteFiles(
+ EasyMock.eq(new File("gs:/fake/path")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
+ .andReturn(fake);
+ EasyMock.expect(
+ mMockResolver.resolveRemoteFiles(
+ EasyMock.eq(new File("gs:/fake2/path2")),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
+ .andReturn(fake2);
+ EasyMock.replay(mMockResolver);
+
+ Set<File> downloadedFile = setter.validateRemoteFilePath();
+ try {
+ assertEquals(2, downloadedFile.size());
+ assertTrue(downloadedFile.contains(object1.remoteFile));
+ assertTrue(downloadedFile.contains(object2.remoteFile));
+
+ assertFalse(object1.remoteFile.equals(object2.remoteFile));
+ } finally {
+ for (File f : downloadedFile) {
+ FileUtil.recursiveDelete(f);
+ }
+ }
+ EasyMock.verify(mMockResolver);
+ }
}
diff --git a/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
index f73e4df..5d9e007 100644
--- a/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
+++ b/tests/src/com/android/tradefed/config/remote/GcsRemoteFileResolverTest.java
@@ -16,12 +16,17 @@
package com.android.tradefed.config.remote;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tradefed.build.BuildRetrievalError;
import com.android.tradefed.build.gcs.GCSDownloaderHelper;
import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
import com.android.tradefed.config.Option;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
import org.junit.Before;
import org.junit.Test;
@@ -30,6 +35,8 @@
import org.mockito.Mockito;
import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
/** Unit tests for {@link GcsRemoteFileResolver}. */
@RunWith(JUnit4.class)
@@ -52,7 +59,8 @@
@Test
public void testResolve() throws Exception {
- mResolver.resolveRemoteFiles(new File("gs:/fake/file"), Mockito.mock(Option.class));
+ mResolver.resolveRemoteFiles(
+ new File("gs:/fake/file"), Mockito.mock(Option.class), new HashMap<>());
Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
}
@@ -64,7 +72,8 @@
.fetchTestResource("gs:/fake/file");
try {
- mResolver.resolveRemoteFiles(new File("gs:/fake/file"), Mockito.mock(Option.class));
+ mResolver.resolveRemoteFiles(
+ new File("gs:/fake/file"), Mockito.mock(Option.class), new HashMap<>());
fail("Should have thrown an exception.");
} catch (ConfigurationException expected) {
assertEquals(
@@ -74,4 +83,50 @@
Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
}
+
+ /** Test that we can request a zip to be unzipped automatically. */
+ @Test
+ public void testResolve_unzip() throws Exception {
+ File testDir = FileUtil.createTempDir("test-resolve-dir");
+ File zipFile = ZipUtil.createZip(testDir);
+ File resolvedFile = null;
+ try {
+ Mockito.doReturn(zipFile).when(mMockHelper).fetchTestResource("gs:/fake/file");
+ Map<String, String> query = new HashMap<>();
+ query.put(DynamicRemoteFileResolver.UNZIP_KEY, /* Case doesn't matter */ "TrUe");
+ resolvedFile =
+ mResolver.resolveRemoteFiles(
+ new File("gs:/fake/file"), Mockito.mock(Option.class), query);
+ // File was unzipped
+ assertTrue(resolvedFile.isDirectory());
+ // Zip file was cleaned
+ assertFalse(zipFile.exists());
+
+ Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
+ } finally {
+ FileUtil.recursiveDelete(testDir);
+ FileUtil.deleteFile(zipFile);
+ FileUtil.recursiveDelete(resolvedFile);
+ }
+ }
+
+ /** Test that if we request to unzip a non-zip file, nothing is done. */
+ @Test
+ public void testResolve_notZip() throws Exception {
+ File testFile = FileUtil.createTempFile("test-resolve-file", ".txt");
+ try {
+ Mockito.doReturn(testFile).when(mMockHelper).fetchTestResource("gs:/fake/file");
+ Map<String, String> query = new HashMap<>();
+ query.put(DynamicRemoteFileResolver.UNZIP_KEY, /* Case doesn't matter */ "TrUe");
+ File resolvedFile =
+ mResolver.resolveRemoteFiles(
+ new File("gs:/fake/file"), Mockito.mock(Option.class), query);
+ // File was not unzipped since it's not one
+ assertEquals(testFile, resolvedFile);
+
+ Mockito.verify(mMockHelper).fetchTestResource("gs:/fake/file");
+ } finally {
+ FileUtil.deleteFile(testFile);
+ }
+ }
}
diff --git a/tests/src/com/android/tradefed/config/remote/HttpRemoteFileResolverTest.java b/tests/src/com/android/tradefed/config/remote/HttpRemoteFileResolverTest.java
new file mode 100644
index 0000000..5419916
--- /dev/null
+++ b/tests/src/com/android/tradefed/config/remote/HttpRemoteFileResolverTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.net.IHttpHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+/** Unit tests for {@link HttpRemoteFileResolver}. */
+@RunWith(JUnit4.class)
+public class HttpRemoteFileResolverTest {
+ private HttpRemoteFileResolver mResolver;
+ private IHttpHelper mHttpDownloader;
+
+ @Before
+ public void setUp() {
+ mHttpDownloader = Mockito.mock(IHttpHelper.class);
+ mResolver =
+ new HttpRemoteFileResolver() {
+ @Override
+ protected IHttpHelper getDownloader() {
+ return mHttpDownloader;
+ }
+ };
+ }
+
+ @Test
+ public void testResolve() throws Exception {
+ File res =
+ mResolver.resolveRemoteFiles(
+ new File("http:/fake/HttpRemoteFileResolverTest"),
+ Mockito.mock(Option.class),
+ new HashMap<>());
+ FileUtil.deleteFile(res);
+
+ Mockito.verify(mHttpDownloader)
+ .doGet(Mockito.eq("http://fake/HttpRemoteFileResolverTest"), Mockito.any());
+ }
+
+ @Test
+ public void testResolve_error() throws Exception {
+ Mockito.doThrow(new IOException("download failure"))
+ .when(mHttpDownloader)
+ .doGet(Mockito.eq("http://fake/HttpRemoteFileResolverTest"), Mockito.any());
+
+ try {
+ mResolver.resolveRemoteFiles(
+ new File("http:/fake/HttpRemoteFileResolverTest"),
+ Mockito.mock(Option.class),
+ new HashMap<>());
+ fail("Should have thrown an exception.");
+ } catch (ConfigurationException expected) {
+ assertEquals(
+ "Failed to download http://fake/HttpRemoteFileResolverTest due to: download failure",
+ expected.getMessage());
+ }
+
+ Mockito.verify(mHttpDownloader)
+ .doGet(Mockito.eq("http://fake/HttpRemoteFileResolverTest"), Mockito.any());
+ }
+}
diff --git a/tests/src/com/android/tradefed/config/remote/LocalFileResolverTest.java b/tests/src/com/android/tradefed/config/remote/LocalFileResolverTest.java
new file mode 100644
index 0000000..86d35f6
--- /dev/null
+++ b/tests/src/com/android/tradefed/config/remote/LocalFileResolverTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.config.remote;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.io.File;
+
+/** Unit tests for {@link LocalFileResolver}. */
+@RunWith(JUnit4.class)
+public class LocalFileResolverTest {
+
+ private LocalFileResolver mResolver;
+
+ @Before
+ public void setUp() {
+ mResolver = new LocalFileResolver();
+ }
+
+ @Test
+ public void testResolveLocalFile() throws Exception {
+ File testFile = FileUtil.createTempFile("test-local-file", ".txt");
+ try {
+ File markedFile = new File("file:" + testFile.getAbsolutePath());
+ File returned = mResolver.resolveRemoteFiles(markedFile, Mockito.mock(Option.class));
+ assertEquals(testFile, returned);
+ } finally {
+ FileUtil.deleteFile(testFile);
+ }
+ }
+
+ @Test
+ public void testResolveLocalFile_notFound() throws Exception {
+ File markedFile = new File("file:whateverpathsomewhere");
+ try {
+ mResolver.resolveRemoteFiles(markedFile, Mockito.mock(Option.class));
+ fail("Should have thrown an exception.");
+ } catch (ConfigurationException expected) {
+ // Expected
+ }
+ }
+}
diff --git a/tests/src/com/android/tradefed/device/CpuStatsCollectorTest.java b/tests/src/com/android/tradefed/device/CpuStatsCollectorTest.java
deleted file mode 100644
index 9bf82bc..0000000
--- a/tests/src/com/android/tradefed/device/CpuStatsCollectorTest.java
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.device.CpuStatsCollector.CpuStats;
-import com.android.tradefed.device.CpuStatsCollector.TimeCategory;
-import com.android.tradefed.testtype.DeviceTestCase;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * Unit tests for {@link CpuStatsCollector}.
- */
-public class CpuStatsCollectorTest extends DeviceTestCase {
- /**
- * Single output for cpustats tool where frequencies are aggregated in the total.
- */
- private final static String[] SINGLE_OUTPUT = {
- "Total,2,3,5,7,11,13,17,350000,19,700000,23,920000,29,1200000,31",
- "cpu0,37,41,43,47,53,59,61,350000,67,700000,71,920000,73,1200000,79",
- "cpu1,83,89,97,101,103,107,109,350000,113,700000,127,920000,131,1200000,137",
- ""};
-
- /**
- * Single output for cpustats tool where frequencies are not aggregated in the total.
- */
- private final static String[] SINGLE_NON_AGGREGATE_OUTPUT = {
- "Total,2,3,5,7,11,13,17",
- "cpu0,37,41,43,47,53,59,61,350000,67,700000,71,920000,73,1200000,79",
- "cpu1,83,89,97,101,103,107,109,350000,113,700000,127,920000,131,1200000,137",
- ""};
-
- /**
- * Multiline output for cpustats tool.
- */
- private final static String[] MULTI_OUTPUT = {
- "Total,246,0,69,283,0,0,0,350000,140,700000,22,920000,122,1200000,318",
- "cpu0,68,0,10,221,0,0,0,350000,70,700000,11,920000,61,1200000,159",
- "cpu1,177,0,60,63,0,0,0,350000,70,700000,11,920000,61,1200000,159",
- "",
- "Total,238,0,75,287,0,0,0,350000,124,700000,12,920000,68,1200000,396",
- "cpu0,53,0,9,238,0,0,0,350000,62,700000,6,920000,34,1200000,198",
- "cpu1,186,0,65,49,0,0,0,350000,62,700000,6,920000,34,1200000,198",
- "",
- "Total,230,0,71,299,0,0,0,350000,0,700000,0,920000,230,1200000,370",
- "cpu0,2,0,3,295,0,0,0,350000,0,700000,0,920000,115,1200000,185",
- "cpu1,228,0,69,3,0,0,0,350000,0,700000,0,920000,115,1200000,185",
- "",
- "Total,248,0,59,293,0,0,0,350000,50,700000,4,920000,330,1200000,216",
- "cpu0,28,0,2,270,0,0,0,350000,25,700000,2,920000,165,1200000,108",
- "cpu1,219,0,56,24,0,0,0,350000,25,700000,2,920000,165,1200000,108",
- "",
- "Total,250,0,63,288,0,0,0,350000,184,700000,22,920000,164,1200000,230",
- "cpu0,79,0,21,201,0,0,0,350000,92,700000,11,920000,82,1200000,115",
- "cpu1,171,0,43,86,0,0,0,350000,92,700000,11,920000,82,1200000,115",
- "",
- "Total,231,0,80,289,0,0,0,350000,96,700000,4,920000,176,1200000,322",
- "cpu0,51,0,7,243,0,0,0,350000,48,700000,2,920000,88,1200000,161",
- "cpu1,181,0,73,47,0,0,0,350000,48,700000,2,920000,88,1200000,161",
- "",
- "Total,248,0,69,283,0,0,0,350000,404,700000,22,920000,26,1200000,150",
- "cpu0,161,0,13,125,0,0,0,350000,202,700000,11,920000,13,1200000,75",
- "cpu1,86,0,55,158,0,0,0,350000,202,700000,11,920000,13,1200000,75",
- "",
- "Total,269,0,97,233,0,0,0,350000,214,700000,18,920000,18,1200000,350",
- "cpu0,40,0,42,218,0,0,0,350000,107,700000,9,920000,9,1200000,175",
- "cpu1,230,0,55,15,0,0,0,350000,107,700000,9,920000,9,1200000,175",
- "",
- "Total,260,0,100,235,0,0,0,350000,152,700000,10,920000,438,1200000,0",
- "cpu0,207,0,72,20,0,0,0,350000,76,700000,5,920000,219,1200000,0",
- "cpu1,53,0,29,214,0,0,0,350000,76,700000,5,920000,219,1200000,0",
- "",
- "Total,248,0,65,287,0,0,0,350000,50,700000,30,920000,336,1200000,184",
- "cpu0,26,0,14,259,0,0,0,350000,25,700000,15,920000,168,1200000,92",
- "cpu1,221,0,51,28,0,0,0,350000,25,700000,15,920000,168,1200000,92",
- ""};
-
- /**
- * Multiline output for cpustats tool where frequencies are not aggregated in the total.
- */
- private final static String[] MULTI_NON_AGGREGATE_OUTPUT = {
- "Total,246,0,69,283,0,0,0",
- "cpu0,68,0,10,221,0,0,0,350000,70,700000,11,920000,61,1200000,159",
- "cpu1,177,0,60,63,0,0,0,350000,70,700000,11,920000,61,1200000,159",
- "",
- "Total,238,0,75,287,0,0,0",
- "cpu0,53,0,9,238,0,0,0,350000,62,700000,6,920000,34,1200000,198",
- "cpu1,186,0,65,49,0,0,0,350000,62,700000,6,920000,34,1200000,198",
- "",
- "Total,230,0,71,299,0,0,0",
- "cpu0,2,0,3,295,0,0,0,350000,0,700000,0,920000,115,1200000,185",
- "cpu1,228,0,69,3,0,0,0,350000,0,700000,0,920000,115,1200000,185",
- "",
- "Total,248,0,59,293,0,0,0",
- "cpu0,28,0,2,270,0,0,0,350000,25,700000,2,920000,165,1200000,108",
- "cpu1,219,0,56,24,0,0,0,350000,25,700000,2,920000,165,1200000,108",
- "",
- "Total,250,0,63,288,0,0,0",
- "cpu0,79,0,21,201,0,0,0,350000,92,700000,11,920000,82,1200000,115",
- "cpu1,171,0,43,86,0,0,0,350000,92,700000,11,920000,82,1200000,115",
- "",
- "Total,231,0,80,289,0,0,0",
- "cpu0,51,0,7,243,0,0,0,350000,48,700000,2,920000,88,1200000,161",
- "cpu1,181,0,73,47,0,0,0,350000,48,700000,2,920000,88,1200000,161",
- "",
- "Total,248,0,69,283,0,0,0",
- "cpu0,161,0,13,125,0,0,0,350000,202,700000,11,920000,13,1200000,75",
- "cpu1,86,0,55,158,0,0,0,350000,202,700000,11,920000,13,1200000,75",
- "",
- "Total,269,0,97,233,0,0,0",
- "cpu0,40,0,42,218,0,0,0,350000,107,700000,9,920000,9,1200000,175",
- "cpu1,230,0,55,15,0,0,0,350000,107,700000,9,920000,9,1200000,175",
- "",
- "Total,260,0,100,235,0,0,0",
- "cpu0,207,0,72,20,0,0,0,350000,76,700000,5,920000,219,1200000,0",
- "cpu1,53,0,29,214,0,0,0,350000,76,700000,5,920000,219,1200000,0",
- "",
- "Total,248,0,65,287,0,0,0",
- "cpu0,26,0,14,259,0,0,0,350000,25,700000,15,920000,168,1200000,92",
- "cpu1,221,0,51,28,0,0,0,350000,25,700000,15,920000,168,1200000,92",
- ""};
-
- private CpuStatsCollector mCollector;
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- mCollector = new CpuStatsCollector(null, 1);
- }
-
- /**
- * Test that a single output from {@code cpustats} is parsed correctly and that {@link CpuStats}
- * contains the correct data and calculates the correct data.
- */
- public void testCpuStatsParser_single() {
- mCollector.getReceiver().processNewLines(SINGLE_OUTPUT);
-
- Map<String, List<CpuStats>> cpuStats = mCollector.getCpuStats();
- assertEquals(3, cpuStats.size());
- assertTrue(cpuStats.containsKey("Total"));
- assertTrue(cpuStats.containsKey("cpu0"));
- assertTrue(cpuStats.containsKey("cpu1"));
-
- assertEquals(1, cpuStats.get("Total").size());
- CpuStats stats = cpuStats.get("Total").get(0);
-
- // Time info
- assertEquals(2, stats.mTimeStats.get(TimeCategory.USER).intValue());
- assertEquals(3, stats.mTimeStats.get(TimeCategory.NICE).intValue());
- assertEquals(5, stats.mTimeStats.get(TimeCategory.SYS).intValue());
- assertEquals(7, stats.mTimeStats.get(TimeCategory.IDLE).intValue());
- assertEquals(11, stats.mTimeStats.get(TimeCategory.IOW).intValue());
- assertEquals(13, stats.mTimeStats.get(TimeCategory.IRQ).intValue());
- assertEquals(17, stats.mTimeStats.get(TimeCategory.SIRQ).intValue());
-
- // Percent info
- assertEquals(100.0 * 2 / 58, stats.getPercentage(TimeCategory.USER), 0.01);
- assertEquals(100.0 * 3 / 58, stats.getPercentage(TimeCategory.NICE), 0.01);
- assertEquals(100.0 * 5 / 58, stats.getPercentage(TimeCategory.SYS), 0.01);
- assertEquals(100.0 * 7 / 58, stats.getPercentage(TimeCategory.IDLE), 0.01);
- assertEquals(100.0 * 11 / 58, stats.getPercentage(TimeCategory.IOW), 0.01);
- assertEquals(100.0 * 13 / 58, stats.getPercentage(TimeCategory.IRQ), 0.01);
- assertEquals(100.0 * 17 / 58, stats.getPercentage(TimeCategory.SIRQ), 0.01);
-
- // Freq info
- assertEquals(4, stats.mFreqStats.size());
- assertEquals(19, stats.mFreqStats.get(350000).intValue());
- assertEquals(23, stats.mFreqStats.get(700000).intValue());
- assertEquals(29, stats.mFreqStats.get(920000).intValue());
- assertEquals(31, stats.mFreqStats.get(1200000).intValue());
- assertEquals((51 / 58.0) * (86630.0 / 102), stats.getEstimatedMhz(), 0.01);
- assertEquals(100.0 * 86630.0 / (102 * 1200), stats.getUsedMhzPercentage(), 0.01);
-
- // cpu0 raw stats
- assertEquals(1, cpuStats.get("cpu0").size());
- stats = cpuStats.get("cpu0").get(0);
- assertEquals(37, stats.mTimeStats.get(TimeCategory.USER).intValue());
- assertEquals(41, stats.mTimeStats.get(TimeCategory.NICE).intValue());
- assertEquals(43, stats.mTimeStats.get(TimeCategory.SYS).intValue());
- assertEquals(47, stats.mTimeStats.get(TimeCategory.IDLE).intValue());
- assertEquals(53, stats.mTimeStats.get(TimeCategory.IOW).intValue());
- assertEquals(59, stats.mTimeStats.get(TimeCategory.IRQ).intValue());
- assertEquals(61, stats.mTimeStats.get(TimeCategory.SIRQ).intValue());
- assertEquals(4, stats.mFreqStats.size());
- assertEquals(67, stats.mFreqStats.get(350000).intValue());
- assertEquals(71, stats.mFreqStats.get(700000).intValue());
- assertEquals(73, stats.mFreqStats.get(920000).intValue());
- assertEquals(79, stats.mFreqStats.get(1200000).intValue());
-
- // cpu1 raw stats
- assertEquals(1, cpuStats.get("cpu1").size());
- stats = cpuStats.get("cpu1").get(0);
- assertEquals(83, stats.mTimeStats.get(TimeCategory.USER).intValue());
- assertEquals(89, stats.mTimeStats.get(TimeCategory.NICE).intValue());
- assertEquals(97, stats.mTimeStats.get(TimeCategory.SYS).intValue());
- assertEquals(101, stats.mTimeStats.get(TimeCategory.IDLE).intValue());
- assertEquals(103, stats.mTimeStats.get(TimeCategory.IOW).intValue());
- assertEquals(107, stats.mTimeStats.get(TimeCategory.IRQ).intValue());
- assertEquals(109, stats.mTimeStats.get(TimeCategory.SIRQ).intValue());
- assertEquals(4, stats.mFreqStats.size());
- assertEquals(113, stats.mFreqStats.get(350000).intValue());
- assertEquals(127, stats.mFreqStats.get(700000).intValue());
- assertEquals(131, stats.mFreqStats.get(920000).intValue());
- assertEquals(137, stats.mFreqStats.get(1200000).intValue());
- }
-
- /**
- * Test that a single output from {@code cpustats} is parsed correctly when frequencies are not
- * aggregated and that {@link CpuStats} contains the correct data and calculates the correct
- * data.
- */
- public void testCpuStatsParser_single_non_aggregate() {
- mCollector.getReceiver().processNewLines(SINGLE_NON_AGGREGATE_OUTPUT);
-
- Map<String, List<CpuStats>> cpuStats = mCollector.getCpuStats();
- assertEquals(3, cpuStats.size());
- assertTrue(cpuStats.containsKey("Total"));
- assertTrue(cpuStats.containsKey("cpu0"));
- assertTrue(cpuStats.containsKey("cpu1"));
-
- assertEquals(1, cpuStats.get("Total").size());
- CpuStats stats = cpuStats.get("Total").get(0);
-
- // Time info
- assertEquals(2, stats.mTimeStats.get(TimeCategory.USER).intValue());
- assertEquals(3, stats.mTimeStats.get(TimeCategory.NICE).intValue());
- assertEquals(5, stats.mTimeStats.get(TimeCategory.SYS).intValue());
- assertEquals(7, stats.mTimeStats.get(TimeCategory.IDLE).intValue());
- assertEquals(11, stats.mTimeStats.get(TimeCategory.IOW).intValue());
- assertEquals(13, stats.mTimeStats.get(TimeCategory.IRQ).intValue());
- assertEquals(17, stats.mTimeStats.get(TimeCategory.SIRQ).intValue());
-
- // Percent info
- assertEquals(100.0 * 2 / 58, stats.getPercentage(TimeCategory.USER), 0.01);
- assertEquals(100.0 * 3 / 58, stats.getPercentage(TimeCategory.NICE), 0.01);
- assertEquals(100.0 * 5 / 58, stats.getPercentage(TimeCategory.SYS), 0.01);
- assertEquals(100.0 * 7 / 58, stats.getPercentage(TimeCategory.IDLE), 0.01);
- assertEquals(100.0 * 11 / 58, stats.getPercentage(TimeCategory.IOW), 0.01);
- assertEquals(100.0 * 13 / 58, stats.getPercentage(TimeCategory.IRQ), 0.01);
- assertEquals(100.0 * 17 / 58, stats.getPercentage(TimeCategory.SIRQ), 0.01);
-
- // Freq info
- assertEquals(0, stats.mFreqStats.size());
- assertNull(stats.getEstimatedMhz());
- assertNull(stats.getUsedMhzPercentage());
- }
-
- /**
- * Tests that multiple lines of {@code cpustats} output are parsed correctly and that
- * {@link CpuStatsCollector} calculates the correct means from the output.
- */
- public void testCpuStatsParser_multi() {
- mCollector.getReceiver().processNewLines(MULTI_OUTPUT);
-
- Map<String, List<CpuStats>> stats = mCollector.getCpuStats();
- assertEquals(3, stats.size());
- assertTrue(stats.containsKey("Total"));
- assertTrue(stats.containsKey("cpu0"));
- assertTrue(stats.containsKey("cpu1"));
-
- assertEquals(10, stats.get("Total").size());
- assertEquals(10, stats.get("cpu0").size());
- assertEquals(10, stats.get("cpu1").size());
-
- assertEquals(53.67, CpuStatsCollector.getTotalPercentageMean(stats.get("Total")), 0.01);
- assertEquals(41.18, CpuStatsCollector.getUserPercentageMean(stats.get("Total")), 0.01);
- assertEquals(12.49, CpuStatsCollector.getSystemPercentageMean(stats.get("Total")), 0.01);
- assertEquals(0.0, CpuStatsCollector.getIowPercentageMean(stats.get("Total")), 0.01);
- assertEquals(0.0, CpuStatsCollector.getIrqPercentageMean(stats.get("Total")), 0.01);
- assertEquals(480.46, CpuStatsCollector.getEstimatedMhzMean(stats.get("Total")), 0.01);
- assertEquals(74.91, CpuStatsCollector.getUsedMhzPercentageMean(stats.get("Total")), 0.01);
-
- assertEquals(30.31, CpuStatsCollector.getTotalPercentageMean(stats.get("cpu0")), 0.01);
- assertEquals(23.87, CpuStatsCollector.getUserPercentageMean(stats.get("cpu0")), 0.01);
- assertEquals(6.44, CpuStatsCollector.getSystemPercentageMean(stats.get("cpu0")), 0.01);
- assertEquals(0.0, CpuStatsCollector.getIowPercentageMean(stats.get("cpu0")), 0.01);
- assertEquals(0.0, CpuStatsCollector.getIrqPercentageMean(stats.get("cpu0")), 0.01);
- assertEquals(246.38, CpuStatsCollector.getEstimatedMhzMean(stats.get("cpu0")), 0.01);
- assertEquals(74.91, CpuStatsCollector.getUsedMhzPercentageMean(stats.get("cpu0")), 0.01);
-
- assertEquals(76.99, CpuStatsCollector.getTotalPercentageMean(stats.get("cpu1")), 0.01);
- assertEquals(58.44, CpuStatsCollector.getUserPercentageMean(stats.get("cpu1")), 0.01);
- assertEquals(18.55, CpuStatsCollector.getSystemPercentageMean(stats.get("cpu1")), 0.01);
- assertEquals(0.0, CpuStatsCollector.getIowPercentageMean(stats.get("cpu1")), 0.01);
- assertEquals(0.0, CpuStatsCollector.getIrqPercentageMean(stats.get("cpu1")), 0.01);
- assertEquals(714.29, CpuStatsCollector.getEstimatedMhzMean(stats.get("cpu1")), 0.01);
- assertEquals(74.91, CpuStatsCollector.getUsedMhzPercentageMean(stats.get("cpu1")), 0.01);
- }
-
- /**
- * Tests that multiple lines of {@code cpustats} output are parsed correctly when frequencies
- * are not aggregated and that {@link CpuStatsCollector} calculates the correct means from the
- * output.
- */
- public void testCpuStatsParser_multi_non_aggregate() {
- mCollector.getReceiver().processNewLines(MULTI_NON_AGGREGATE_OUTPUT);
-
- Map<String, List<CpuStats>> stats = mCollector.getCpuStats();
- assertEquals(3, stats.size());
- assertTrue(stats.containsKey("Total"));
- assertTrue(stats.containsKey("cpu0"));
- assertTrue(stats.containsKey("cpu1"));
-
- assertEquals(10, stats.get("Total").size());
- assertEquals(10, stats.get("cpu0").size());
- assertEquals(10, stats.get("cpu1").size());
-
- assertEquals(53.67, CpuStatsCollector.getTotalPercentageMean(stats.get("Total")), 0.01);
- assertEquals(41.18, CpuStatsCollector.getUserPercentageMean(stats.get("Total")), 0.01);
- assertEquals(12.49, CpuStatsCollector.getSystemPercentageMean(stats.get("Total")), 0.01);
- assertEquals(0.0, CpuStatsCollector.getIowPercentageMean(stats.get("Total")), 0.01);
- assertEquals(0.0, CpuStatsCollector.getIrqPercentageMean(stats.get("Total")), 0.01);
- assertNull(CpuStatsCollector.getEstimatedMhzMean(stats.get("Total")));
- assertNull(CpuStatsCollector.getUsedMhzPercentageMean(stats.get("Total")));
- }
-}
diff --git a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java b/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java
deleted file mode 100644
index 82edc22..0000000
--- a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.command.remote.DeviceDescriptor;
-import com.android.tradefed.device.IDeviceMonitor.DeviceLister;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Load test for {@link DeviceUtilStatsMonitor} Used to ensure memory used by monitor under heavy
- * load is reasonable
- */
-public class DeviceUtilStatsMonitorLoadTest extends TestCase {
-
- private static final int NUM_DEVICES = 100;
-
- private IDeviceManager mMockDeviceManager;
- private DeviceUtilStatsMonitor mDeviceUtilMonitor;
-
- @Override
- public void setUp() {
- mMockDeviceManager = EasyMock.createNiceMock(IDeviceManager.class);
- mDeviceUtilMonitor = new DeviceUtilStatsMonitor() {
- @Override
- IDeviceManager getDeviceManager() {
- return mMockDeviceManager;
- }
- };
- mDeviceUtilMonitor.setDeviceLister(
- new DeviceLister() {
- @Override
- public List<DeviceDescriptor> listDevices() {
- return mMockDeviceManager.listAllDevices();
- }
-
- @Override
- public DeviceDescriptor getDeviceDescriptor(String serial) {
- return mMockDeviceManager.getDeviceDescriptor(serial);
- }
- });
- mDeviceUtilMonitor.calculateMaxSamples();
- }
-
- /**
- * Simulate a heavy load by generating constant allocation events length for
- * all NUM_DEVICES devices.
- * <p/>
- * Intended to be run under a profiler.
- * @throws InterruptedException
- */
- public void testManyRecords() throws InterruptedException {
- List<DeviceDescriptor> deviceList = new ArrayList<>(NUM_DEVICES);
- for (int i =0; i <NUM_DEVICES; i++) {
- DeviceDescriptor device = createDeviceDesc("serial" + i, DeviceAllocationState.Allocated);
- deviceList.add(device);
- }
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andStubReturn(deviceList);
- EasyMock.replay(mMockDeviceManager);
-
- for (int i = 0; i < mDeviceUtilMonitor.getMaxSamples(); i++) {
- mDeviceUtilMonitor.getSamplingTask().run();
- }
- // This takes ~ 1.9 MB in heap if DeviceUtilStatsMonitor uses a LinkedList<Byte> to
- // store samples
- // takes ~ 65K if CircularByteArray is used
- Thread.sleep(5 * 60 * 1000);
- }
-
- /**
- * Helper method to create a {@link DeviceDescriptor} using only serial and state.
- */
- private DeviceDescriptor createDeviceDesc(String serial, DeviceAllocationState state) {
- return new DeviceDescriptor(serial, false, state, null, null, null, null, null);
- }
-
- public static void main(String[] args) {
- //new DeviceUtilStatsMonitorLoadTest().testManyRecords();
- }
-}
diff --git a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java b/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java
deleted file mode 100644
index 87bee7f..0000000
--- a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.tradefed.command.remote.DeviceDescriptor;
-import com.android.tradefed.device.DeviceUtilStatsMonitor.UtilizationDesc;
-import com.android.tradefed.device.IDeviceMonitor.DeviceLister;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Simple unit tests for {@link DeviceUtilStatsMonitor}
- */
-public class DeviceUtilStatsMonitorTest extends TestCase {
-
- private IDeviceManager mMockDeviceManager;
- private DeviceUtilStatsMonitor mDeviceUtilMonitor;
-
- @Override
- public void setUp() {
- mMockDeviceManager = EasyMock.createNiceMock(IDeviceManager.class);
- mDeviceUtilMonitor = new DeviceUtilStatsMonitor() {
- @Override
- IDeviceManager getDeviceManager() {
- return mMockDeviceManager;
- }
- };
- mDeviceUtilMonitor.setDeviceLister(
- new DeviceLister() {
- @Override
- public List<DeviceDescriptor> listDevices() {
- return mMockDeviceManager.listAllDevices();
- }
-
- @Override
- public DeviceDescriptor getDeviceDescriptor(String serial) {
- return mMockDeviceManager.getDeviceDescriptor(serial);
- }
- });
- mDeviceUtilMonitor.calculateMaxSamples();
- }
-
- public void testEmpty() {
- EasyMock.replay(mMockDeviceManager);
- assertEquals(0, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
- }
-
- /**
- * Test case where device has been available but never allocated
- */
- public void testOnlyAvailable() {
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
- DeviceAllocationState.Available));
- EasyMock.replay(mMockDeviceManager);
-
- mDeviceUtilMonitor.getSamplingTask().run();
- UtilizationDesc desc = mDeviceUtilMonitor.getUtilizationStats();
- assertEquals(0, desc.mTotalUtil);
- assertEquals(1, desc.mDeviceUtil.size());
- assertEquals(0L, (long)desc.mDeviceUtil.get("serial0"));
- }
-
- /**
- * Test case where device has been allocated but never available
- */
- public void testOnlyAllocated() {
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
- DeviceAllocationState.Allocated));
- EasyMock.replay(mMockDeviceManager);
-
- mDeviceUtilMonitor.getSamplingTask().run();
- UtilizationDesc desc = mDeviceUtilMonitor.getUtilizationStats();
- assertEquals(100L, desc.mTotalUtil);
- assertEquals(1, desc.mDeviceUtil.size());
- assertEquals(100L, (long)desc.mDeviceUtil.get("serial0"));
- }
-
- /**
- * Test case where samples exceed max
- */
- public void testExceededSamples() {
- mDeviceUtilMonitor.setMaxSamples(2);
- // first return allocated, then return samples with device missing
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
- DeviceAllocationState.Allocated));
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(DeviceAllocationState.Available));
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(DeviceAllocationState.Available));
- EasyMock.replay(mMockDeviceManager);
-
- mDeviceUtilMonitor.getSamplingTask().run();
- // only 1 sample - allocated
- assertEquals(100L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
- mDeviceUtilMonitor.getSamplingTask().run();
- // 1 out of 2
- assertEquals(50L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
- mDeviceUtilMonitor.getSamplingTask().run();
- // 0 out of 2
- assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
- }
-
- /**
- * Test case where device disappears. Ensure util numbers are calculated until > max samples
- * have been collected with it missing
- */
- public void testMissingDevice() {
- mDeviceUtilMonitor.setMaxSamples(2);
- // first return allocated, then return samples with device missing
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
- DeviceAllocationState.Allocated));
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
- EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
- EasyMock.replay(mMockDeviceManager);
-
- // only 1 sample - allocated
- mDeviceUtilMonitor.getSamplingTask().run();
- assertEquals(100L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-
- // 1 out of 2
- mDeviceUtilMonitor.getSamplingTask().run();
- assertEquals(50L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-
- // 0 out of 2
- mDeviceUtilMonitor.getSamplingTask().run();
- assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
-
- // now removed
- mDeviceUtilMonitor.getSamplingTask().run();
- assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mDeviceUtil.size());
-
- }
-
- private List<DeviceDescriptor> buildDeviceList(DeviceAllocationState... states) {
- List<DeviceDescriptor> deviceList = new ArrayList<>(states.length);
- for (int i =0; i < states.length; i++) {
- DeviceDescriptor device = createDeviceDesc("serial" + i, states[i]);
- deviceList.add(device);
- }
- return deviceList;
- }
-
- /**
- * Helper method to create a {@link DeviceDescriptor} using only serial and state.
- */
- private DeviceDescriptor createDeviceDesc(String serial, DeviceAllocationState state) {
- return new DeviceDescriptor(serial, false, state, null, null, null, null, null);
- }
-}
diff --git a/tests/src/com/android/tradefed/device/NativeDeviceTest.java b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
index 091fc5a..9afd711 100644
--- a/tests/src/com/android/tradefed/device/NativeDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/NativeDeviceTest.java
@@ -70,6 +70,7 @@
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -2166,6 +2167,134 @@
}
}
+ /** Test get Process pid by process name */
+ @Test
+ public void testGetProcessPid() throws Exception {
+ final String fakePid = "914";
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn(fakePid).when(spy).executeShellCommand("pidof system_server");
+ EasyMock.replay(mMockIDevice);
+ assertEquals(fakePid, spy.getProcessPid("system_server"));
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get Process pid by process name with adb shell return of extra new line */
+ @Test
+ public void testGetProcessPidWithNewLine() throws Exception {
+ final String fakePid = "914";
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn(fakePid + "\n").when(spy).executeShellCommand("pidof system_server");
+ EasyMock.replay(mMockIDevice);
+ assertEquals(fakePid, spy.getProcessPid("system_server"));
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get Process pid return null with invalid shell command output */
+ @Test
+ public void testGetProcessPidInvalidOutput() throws Exception {
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn("invalid output").when(spy).executeShellCommand("pidof system_server");
+ EasyMock.replay(mMockIDevice);
+ assertNull(spy.getProcessPid("system_server"));
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get Process pid return null with shell command empty output */
+ @Test
+ public void testGetProcessPidEmptyOutput() throws Exception {
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn("").when(spy).executeShellCommand("pidof system_server");
+ EasyMock.replay(mMockIDevice);
+ assertNull(spy.getProcessPid("system_server"));
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get ProcessInfo by process name */
+ @Test
+ public void testGetProcessWithStartTimeByName() throws Exception {
+ final String fakePid = "914";
+ final String fakeCreationTime = "1559091922";
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn(fakePid).when(spy).executeShellCommand("pidof system_server");
+ doReturn(fakeCreationTime).when(spy).executeShellCommand("stat -c%Z /proc/" + fakePid);
+ doReturn("system").when(spy).executeShellCommand("stat -c%U /proc/" + fakePid);
+ EasyMock.replay(mMockIDevice);
+ assertEquals(Integer.parseInt(fakePid), spy.getProcessByName("system_server").getPid());
+ assertEquals(
+ Long.parseLong(fakeCreationTime),
+ spy.getProcessByName("system_server").getStartTime());
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get ProcessInfo by process name return null for invalid process */
+ @Test
+ public void testGetProcessWithStartTimeByNameInvalidProcess() throws Exception {
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn("").when(spy).executeShellCommand("pidof system_server");
+ EasyMock.replay(mMockIDevice);
+ assertNull(spy.getProcessByName("system_server"));
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get boot history */
+ @Test
+ public void testGetBootHistory() throws Exception {
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn(
+ "kernel_panic,1556587278\n"
+ + " reboot,,1556238008\n"
+ + " reboot,,1556237796\n"
+ + " reboot,,1556237725\n")
+ .when(spy)
+ .getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+ Map<Long, String> history = new LinkedHashMap<Long, String>();
+ history.put(1556587278L, "kernel_panic");
+ history.put(1556238008L, "reboot");
+ history.put(1556237796L, "reboot");
+ history.put(1556237725L, "reboot");
+ EasyMock.replay(mMockIDevice);
+ assertEquals(history, spy.getBootHistory());
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get empty boot history */
+ @Test
+ public void testGetBootHistoryEmpty() throws Exception {
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn("").when(spy).getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+ EasyMock.replay(mMockIDevice);
+ assertTrue(spy.getBootHistory().isEmpty());
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get invalid boot history */
+ @Test
+ public void testGetBootHistoryInvalid() throws Exception {
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn("invalid output").when(spy).getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+ EasyMock.replay(mMockIDevice);
+ assertTrue(spy.getBootHistory().isEmpty());
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test get boot history since */
+ @Test
+ public void testGetBootHistorySince() throws Exception {
+ TestableAndroidNativeDevice spy = Mockito.spy(mTestDevice);
+ doReturn(
+ "kernel_panic,1556587278\n"
+ + " reboot,,1556238008\n"
+ + " reboot,,1556237796\n"
+ + " reboot,,1556237725\n")
+ .when(spy)
+ .getProperty(DeviceProperties.BOOT_REASON_HISTORY);
+ Map<Long, String> history = new LinkedHashMap<Long, String>();
+ history.put(1556587278L, "kernel_panic");
+ EasyMock.replay(mMockIDevice);
+ assertEquals(history, spy.getBootHistorySince(1556238008L));
+ EasyMock.verify(mMockIDevice);
+ }
+
/** Test validating valid MAC addresses */
@Test
public void testIsMacAddress() {
@@ -2630,4 +2759,5 @@
assertEquals(2, result.size());
EasyMock.verify(mMockIDevice);
}
+
}
diff --git a/tests/src/com/android/tradefed/device/ReconnectingRecoveryTest.java b/tests/src/com/android/tradefed/device/ReconnectingRecoveryTest.java
deleted file mode 100644
index f87e4e3..0000000
--- a/tests/src/com/android/tradefed/device/ReconnectingRecoveryTest.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.device;
-
-import com.android.ddmlib.IDevice;
-import com.android.tradefed.util.IRunUtil;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-/**
- * Unit tests for {@link ReconnectingRecovery}.
- */
-public class ReconnectingRecoveryTest extends TestCase {
-
- private static final String SERIAL = "serial";
- private IDevice mMockDevice;
- private IDeviceStateMonitor mMockMonitor;
- private IRunUtil mMockRunUtil;
- private ReconnectingRecovery mRecovery;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- mMockRunUtil = EasyMock.createMock(IRunUtil.class);
- mRecovery = new ReconnectingRecovery() {
- @Override
- protected IRunUtil getRunUtil() {
- return mMockRunUtil;
- }
- };
- mMockMonitor = EasyMock.createMock(IDeviceStateMonitor.class);
- EasyMock.expect(mMockMonitor.getSerialNumber()).andStubReturn(SERIAL);
- mMockDevice = EasyMock.createMock(IDevice.class);
- }
-
- /**
- * Test {@link ReconnectingRecovery#recoverDevice(IDeviceStateMonitor, boolean)}
- * when device is actually recoverable upon the first attempt.
- */
- public final void testRecoverDevice_successOnFirstTry() throws DeviceNotAvailableException {
- expectInitialDisconnectConnectAttempt();
- EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(mMockDevice);
- EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
- EasyMock.expect(mMockMonitor.waitForDeviceAvailable()).andReturn(mMockDevice);
- replayMocks();
- mRecovery.recoverDevice(mMockMonitor, false);
- verifyMocks();
- }
-
- /**
- * Test {@link ReconnectingRecovery#recoverDevice(IDeviceStateMonitor, boolean)}
- * when device is actually recoverable, but not on the first attempt.
- */
- public final void testRecoverDevice_successRetrying() throws DeviceNotAvailableException {
- expectInitialDisconnectConnectAttempt();
- // fail 1st attempt
- EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(null);
- // then it should retry at least once
- EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), connectCommand())).andReturn(
- null);
- EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andReturn(mMockDevice);
- EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
- EasyMock.expect(mMockMonitor.waitForDeviceAvailable()).andReturn(mMockDevice);
- replayMocks();
- mRecovery.recoverDevice(mMockMonitor, false);
- verifyMocks();
- }
-
- /**
- * Test {@link ReconnectingRecovery#recoverDevice(IDeviceStateMonitor, boolean)}
- * when device is actually irrecoverable.
- */
- public final void testRecoverDevice_failure() throws DeviceNotAvailableException {
- expectInitialDisconnectConnectAttempt();
- EasyMock.expect(mMockMonitor.waitForDeviceOnline()).andStubReturn(null);
- EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), connectCommand()))
- .andStubReturn(null);
- EasyMock.expect(mMockMonitor.waitForDeviceShell(EasyMock.anyLong())).andReturn(true);
- EasyMock.expect(mMockMonitor.waitForDeviceAvailable()).andReturn(null);
- replayMocks();
- try {
- mRecovery.recoverDevice(mMockMonitor, false);
- fail("DeviceUnresponsiveException not thrown");
- } catch (DeviceUnresponsiveException e) {
- assertTrue(true);
- }
- verifyMocks();
- }
-
- private void expectInitialDisconnectConnectAttempt() {
- EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), disconnectCommand()))
- .andStubReturn(null);
- EasyMock.expect(mMockRunUtil.runTimedCmd(EasyMock.anyLong(), connectCommand()))
- .andStubReturn(null);
- }
-
- public final void testRecoverDeviceBootloader_notImplemented()
- throws DeviceNotAvailableException {
- replayMocks();
- try {
- mRecovery.recoverDeviceBootloader(mMockMonitor);
- fail("should have thrown an UnsupportedOperationException");
- } catch (java.lang.UnsupportedOperationException e) {
- // expected
- }
- verifyMocks();
- }
-
- private String[] disconnectCommand() {
- return new String[] { EasyMock.eq("adb"), EasyMock.eq("disconnect"), EasyMock.eq(SERIAL) };
- }
-
- private String[] connectCommand() {
- return new String[] { EasyMock.eq("adb"), EasyMock.eq("connect"), EasyMock.eq(SERIAL) };
- }
-
- /**
- * Verify all the mock objects
- */
- private void verifyMocks() {
- EasyMock.verify(mMockRunUtil, mMockMonitor, mMockDevice);
- }
-
- /**
- * Switch all the mock objects to replay mode
- */
- private void replayMocks() {
- EasyMock.replay(mMockRunUtil, mMockMonitor, mMockDevice);
- }
-}
diff --git a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
index 8a136fb..1f0de39 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceFuncTest.java
@@ -50,7 +50,7 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.nio.file.Files;
+import java.net.URLConnection;
import java.util.Set;
import javax.imageio.ImageIO;
@@ -836,8 +836,9 @@
assertNotNull(screenshot);
File testFile = FileUtil.createTempFile("test-screenshot", ".testpng");
try {
- FileUtil.writeToFile(screenshot.createInputStream(), testFile);
- assertEquals("image/png", Files.probeContentType(testFile.toPath()));
+ assertEquals(
+ "image/png",
+ URLConnection.guessContentTypeFromStream(screenshot.createInputStream()));
} finally {
FileUtil.deleteFile(testFile);
StreamUtil.close(screenshot);
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index d334d10..c3c1b00 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -48,7 +48,6 @@
import com.android.tradefed.util.KeyguardControllerState;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
-import com.android.tradefed.util.UserUtil;
import com.android.tradefed.util.ZipUtil2;
import junit.framework.TestCase;
@@ -1186,7 +1185,7 @@
*/
private void setEncryptedUnsupportedExpectations() throws Exception {
setEnableAdbRootExpectations();
- injectShellResponse("vdc cryptfs enablecrypto", "\r\n");
+ EasyMock.expect(mMockIDevice.getProperty("ro.crypto.state")).andReturn("unsupported");
}
/**
@@ -1194,9 +1193,7 @@
*/
private void setEncryptedSupported() throws Exception {
setEnableAdbRootExpectations();
- injectShellResponse("vdc cryptfs enablecrypto",
- "500 29805 Usage: cryptfs enablecrypto <wipe|inplace> "
- + "default|password|pin|pattern [passwd] [noui]\r\n");
+ EasyMock.expect(mMockIDevice.getProperty("ro.crypto.state")).andReturn("encrypted");
}
/**
@@ -2488,6 +2485,46 @@
assertEquals(3, actual.get(1).intValue());
}
+ /** Test that a single user is handled by {@link TestDevice#listUsers()}. */
+ public void testListUsersInfo_oneUser() throws Exception {
+ final String listUsersCommand = "pm list users";
+ injectShellResponse(
+ listUsersCommand, ArrayUtil.join("\r\n", "Users:", "UserInfo{0:Foo:13} running"));
+ replayMocks();
+ Map<Integer, UserInfo> actual = mTestDevice.getUserInfos();
+ assertNotNull(actual);
+ assertEquals(1, actual.size());
+ UserInfo user0 = actual.get(0);
+ assertEquals(0, user0.userId());
+ assertEquals("Foo", user0.userName());
+ assertEquals(0x13, user0.flag());
+ assertEquals(true, user0.isRunning());
+ }
+
+ /** Test that multiple user is handled by {@link TestDevice#listUsers()}. */
+ public void testListUsersInfo_multiUsers() throws Exception {
+ final String listUsersCommand = "pm list users";
+ injectShellResponse(
+ listUsersCommand,
+ ArrayUtil.join(
+ "\r\n", "Users:", "UserInfo{0:Foo:13} running", "UserInfo{10:FooBar:14}"));
+ replayMocks();
+ Map<Integer, UserInfo> actual = mTestDevice.getUserInfos();
+ assertNotNull(actual);
+ assertEquals(2, actual.size());
+ UserInfo user0 = actual.get(0);
+ assertEquals(0, user0.userId());
+ assertEquals("Foo", user0.userName());
+ assertEquals(0x13, user0.flag());
+ assertEquals(true, user0.isRunning());
+
+ UserInfo user10 = actual.get(10);
+ assertEquals(10, user10.userId());
+ assertEquals("FooBar", user10.userName());
+ assertEquals(0x14, user10.flag());
+ assertEquals(false, user10.isRunning());
+ }
+
/**
* Test that multi user output is handled by {@link TestDevice#getMaxNumberOfUsersSupported()}.
*/
@@ -2982,10 +3019,10 @@
+ "UserInfo{12:Secondary:0}\n\t"
+ "UserInfo{13:Managed:%x}\n\t"
+ "UserInfo{100:Restricted:%x}\n\t",
- UserUtil.FLAG_PRIMARY,
- UserUtil.FLAG_GUEST,
- UserUtil.FLAG_MANAGED_PROFILE,
- UserUtil.FLAG_RESTRICTED);
+ UserInfo.FLAG_PRIMARY,
+ UserInfo.FLAG_GUEST,
+ UserInfo.FLAG_MANAGED_PROFILE,
+ UserInfo.FLAG_RESTRICTED);
}
@Override
@@ -3151,8 +3188,12 @@
@Override
public String executeShellCommand(String command)
throws DeviceNotAvailableException {
- test.setName(getClass().getCanonicalName() + "#testSwitchUser_delay");
- test.start();
+ if (!started) {
+ started = true;
+ test.setDaemon(true);
+ test.setName(getClass().getCanonicalName() + "#testSwitchUser_delay");
+ test.start();
+ }
return "";
}
@@ -3176,6 +3217,7 @@
return 100;
}
+ boolean started = false;
Thread test =
new Thread(
new Runnable() {
@@ -4169,9 +4211,7 @@
return true;
}
};
- injectShellResponse(
- "vdc cryptfs enablecrypto",
- "500 8674 Usage with ext4crypt: cryptfs enablecrypto inplace default noui\r\n");
+ EasyMock.expect(mMockIDevice.getProperty("ro.crypto.state")).andReturn("encrypted");
EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
assertTrue(mTestDevice.isEncryptionSupported());
EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -4191,7 +4231,7 @@
return true;
}
};
- injectShellResponse("vdc cryptfs enablecrypto", "500 8674 Command not recognized\r\n");
+ EasyMock.expect(mMockIDevice.getProperty("ro.crypto.state")).andReturn("unsupported");
EasyMock.replay(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
assertFalse(mTestDevice.isEncryptionSupported());
EasyMock.verify(mMockIDevice, mMockStateMonitor, mMockDvcMonitor);
@@ -4310,7 +4350,7 @@
mMockWifi.cleanUp();
replayMocks();
mTestDevice.getIpAddress();
- mTestDevice.postInvocationTearDown();
+ mTestDevice.postInvocationTearDown(null);
verifyMocks();
}
@@ -4381,4 +4421,41 @@
StreamUtil.close(source);
verifyMocks();
}
+
+ /** Test {@link TestDevice#doesFileExist(String)}. */
+ public void testDoesFileExists() throws Exception {
+ injectShellResponse("ls \"/data/local/tmp/file\"", "file");
+ EasyMock.replay(mMockIDevice);
+ assertTrue(mTestDevice.doesFileExist("/data/local/tmp/file"));
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /** Test {@link TestDevice#doesFileExist(String)} when the file does not exists. */
+ public void testDoesFileExists_notExists() throws Exception {
+ injectShellResponse(
+ "ls \"/data/local/tmp/file\"",
+ "ls: cannot access 'file': No such file or directory\n");
+ EasyMock.replay(mMockIDevice);
+ assertFalse(mTestDevice.doesFileExist("/data/local/tmp/file"));
+ EasyMock.verify(mMockIDevice);
+ }
+
+ /**
+ * Test {@link TestDevice#doesFileExist(String)} when the file exists on an sdcard from another
+ * user.
+ */
+ public void testDoesFileExists_sdcard() throws Exception {
+ mTestDevice =
+ new TestableTestDevice() {
+ @Override
+ public int getCurrentUser()
+ throws DeviceNotAvailableException, DeviceRuntimeException {
+ return 10;
+ }
+ };
+ injectShellResponse("ls \"/storage/emulated/10/file\"", "file");
+ EasyMock.replay(mMockIDevice);
+ assertTrue(mTestDevice.doesFileExist("/sdcard/file"));
+ EasyMock.verify(mMockIDevice);
+ }
}
diff --git a/tests/src/com/android/tradefed/device/TopHelperTest.java b/tests/src/com/android/tradefed/device/TopHelperTest.java
deleted file mode 100644
index a216b7e..0000000
--- a/tests/src/com/android/tradefed/device/TopHelperTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.device;
-
-import com.android.tradefed.device.TopHelper.TopStats;
-
-import junit.framework.TestCase;
-
-import org.easymock.EasyMock;
-
-import java.util.List;
-
-/**
- * Unit tests for {@link TopHelper}
- */
-public class TopHelperTest extends TestCase {
- private ITestDevice mMockDevice;
- private TopHelper mTop;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- mMockDevice = EasyMock.createMock(ITestDevice.class);
- mTop = new TopHelper(mMockDevice);
- }
-
- /**
- * Test that all the output from top invoked once is supported and parsed (or not parsed).
- */
- public void testTopParser_parse() {
- final String lines = ("User 7%, System 5%, IOW 3%, IRQ 2%\r\n" +
- "User 3 + Nice 0 + Sys 6 + Idle 100 + IOW 0 + IRQ 0 + SIRQ 0 = 109\r\n" +
- "\r\n" +
- " PID TID PR CPU% S VSS RSS PCY UID Thread Proc\r\n" +
- " 1388 1388 0 11% R 1160K 576K fg shell top top\r\n" +
- " 2 2 0 0% S 0K 0K fg root kthreadd\r\n" +
- "\r\n" +
- "\r\n" +
- "\r\n");
-
- mTop.getReceiver().processNewLines(lines.split("\r\n"));
- List<TopStats> stats = mTop.getTopStats();
-
- assertEquals(1, stats.size());
-
- assertEquals(17.0, stats.get(0).mTotalPercent, 0.0001);
- assertEquals(7.0, stats.get(0).mUserPercent, 0.0001);
- assertEquals(5.0, stats.get(0).mSystemPercent, 0.0001);
- assertEquals(3.0, stats.get(0).mIowPercent, 0.0001);
- assertEquals(2.0, stats.get(0).mIrqPercent, 0.0001);
- }
-
- /**
- * Test that the parser returns the correct averages for various ranges.
- */
- public void testTopParser_stats() {
- final String lines = (
- "User 15%, System 11%, IOW 7%, IRQ 3%\r\n" +
- "User 16%, System 12%, IOW 8%, IRQ 4%\r\n" +
- "User 17%, System 13%, IOW 9%, IRQ 5%\r\n" +
- "User 18%, System 14%, IOW 10%, IRQ 6%\r\n" +
- "User 19%, System 15%, IOW 11%, IRQ 7%\r\n" +
- "User 20%, System 16%, IOW 12%, IRQ 8%\r\n" +
- "User 21%, System 17%, IOW 13%, IRQ 9%\r\n");
-
- List<TopStats> stats = mTop.getTopStats();
-
- assertEquals(0, stats.size());
-
- assertNull(TopHelper.getTotalAverage(stats));
- assertNull(TopHelper.getUserAverage(stats));
- assertNull(TopHelper.getSystemAverage(stats));
- assertNull(TopHelper.getIowAverage(stats));
- assertNull(TopHelper.getIrqAverage(stats));
-
- mTop.getReceiver().processNewLines(lines.split("\r\n"));
- stats = mTop.getTopStats();
-
- assertEquals(7, mTop.getTopStats().size());
-
- assertEquals(48.0, TopHelper.getTotalAverage(stats.subList(0, 7)), 0.001);
- assertEquals(18.0, TopHelper.getUserAverage(stats.subList(0, 7)), 0.001);
- assertEquals(14.0, TopHelper.getSystemAverage(stats.subList(0, 7)), 0.001);
- assertEquals(10.0, TopHelper.getIowAverage(stats.subList(0, 7)), 0.001);
- assertEquals(6.0, TopHelper.getIrqAverage(stats.subList(0, 7)), 0.001);
-
- assertEquals(44.0, TopHelper.getTotalAverage(stats.subList(0, 7 - 2)), 0.001);
- assertEquals(17.0, TopHelper.getUserAverage(stats.subList(0, 7 - 2)), 0.001);
- assertEquals(13.0, TopHelper.getSystemAverage(stats.subList(0, 7 - 2)), 0.001);
- assertEquals(9.0, TopHelper.getIowAverage(stats.subList(0, 7 - 2)), 0.001);
- assertEquals(5.0, TopHelper.getIrqAverage(stats.subList(0, 7 - 2)), 0.001);
-
- assertEquals(52.0, TopHelper.getTotalAverage(stats.subList(2, 7)), 0.001);
- assertEquals(19.0, TopHelper.getUserAverage(stats.subList(2, 7)), 0.001);
- assertEquals(15.0, TopHelper.getSystemAverage(stats.subList(2, 7)), 0.001);
- assertEquals(11.0, TopHelper.getIowAverage(stats.subList(2, 7)), 0.001);
- assertEquals(7.0, TopHelper.getIrqAverage(stats.subList(2, 7)), 0.001);
-
- assertNull(TopHelper.getTotalAverage(stats.subList(3, 3)));
- assertNull(TopHelper.getUserAverage(stats.subList(3, 3)));
- assertNull(TopHelper.getSystemAverage(stats.subList(3, 3)));
- assertNull(TopHelper.getIowAverage(stats.subList(3, 3)));
- assertNull(TopHelper.getIrqAverage(stats.subList(3, 3)));
- }
-}
diff --git a/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java
new file mode 100644
index 0000000..7bf87e6
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/cloud/ManagedRemoteDeviceTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device.cloud;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.ddmlib.IDevice;
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.device.IDeviceMonitor;
+import com.android.tradefed.device.IDeviceStateMonitor;
+import com.android.tradefed.device.TestDeviceOptions;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link ManagedRemoteDevice}. */
+@RunWith(JUnit4.class)
+public class ManagedRemoteDeviceTest {
+ private ManagedRemoteDevice mDevice;
+ private IDevice mIDevice;
+ private IDeviceStateMonitor mStateMonitor;
+ private IDeviceMonitor mDeviceMonitor;
+
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ try {
+ GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
+ } catch (IllegalStateException e) {
+ // Ignore
+ }
+ }
+
+ @Before
+ public void setUp() {
+ mIDevice = Mockito.mock(IDevice.class);
+ mStateMonitor = Mockito.mock(IDeviceStateMonitor.class);
+ mDeviceMonitor = Mockito.mock(IDeviceMonitor.class);
+ mDevice = new ManagedRemoteDevice(mIDevice, mStateMonitor, mDeviceMonitor);
+ }
+
+ @Test
+ public void testGetOptions() {
+ TestDeviceOptions originalOptions = new TestDeviceOptions();
+ mDevice.setOptions(originalOptions);
+ TestDeviceOptions get = mDevice.getOptions();
+ assertFalse(get.equals(originalOptions));
+ TestDeviceOptions get2 = mDevice.getOptions();
+ assertTrue(get2.equals(get));
+ }
+}
diff --git a/tests/src/com/android/tradefed/device/cloud/NestedRemoteDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/NestedRemoteDeviceTest.java
index 4ac2b17..89616c5 100644
--- a/tests/src/com/android/tradefed/device/cloud/NestedRemoteDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/NestedRemoteDeviceTest.java
@@ -74,7 +74,7 @@
@After
public void tearDown() throws Exception {
- mDevice.postInvocationTearDown();
+ mDevice.postInvocationTearDown(null);
}
/** Test that reset device returns true in case of success */
diff --git a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
index 3540bca..8a7eba7 100644
--- a/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/cloud/RemoteAndroidVirtualDeviceTest.java
@@ -49,6 +49,7 @@
import com.google.common.net.HostAndPort;
import org.easymock.EasyMock;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -56,6 +57,7 @@
import org.mockito.Mockito;
import java.io.File;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -134,6 +136,11 @@
mMockBuildInfo = new BuildInfo();
}
+ @After
+ public void tearDown() {
+ FileUtil.deleteFile(mTestDevice.getExecuteShellCommandLog());
+ }
+
/**
* Test that an exception thrown in the parser should be propagated to the top level and should
* not be caught.
@@ -241,7 +248,7 @@
@Test
public void testPreInvocationSetup() throws Exception {
IBuildInfo mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
- TestableRemoteAndroidVirtualDevice testDevice =
+ mTestDevice =
new TestableRemoteAndroidVirtualDevice() {
@Override
protected void launchGce(IBuildInfo buildInfo) throws TargetSetupError {
@@ -272,7 +279,7 @@
.andReturn(mMockIDevice);
EasyMock.expect(mMockIDevice.getState()).andReturn(DeviceState.ONLINE);
replayMocks(mMockBuildInfo);
- testDevice.preInvocationSetup(mMockBuildInfo);
+ mTestDevice.preInvocationSetup(mMockBuildInfo);
verifyMocks(mMockBuildInfo);
Mockito.verify(mGceHandler).logStableHostImageInfos(mMockBuildInfo);
@@ -285,7 +292,7 @@
@Test
public void testPreInvocationSetup_fails() throws Exception {
IBuildInfo mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
- TestableRemoteAndroidVirtualDevice testDevice =
+ mTestDevice =
new TestableRemoteAndroidVirtualDevice() {
@Override
protected void launchGce(IBuildInfo buildInfo) throws TargetSetupError {
@@ -302,7 +309,7 @@
EasyMock.expect(mMockIDevice.getState()).andReturn(DeviceState.OFFLINE).times(2);
replayMocks(mMockBuildInfo);
try {
- testDevice.preInvocationSetup(mMockBuildInfo);
+ mTestDevice.preInvocationSetup(mMockBuildInfo);
fail("Should have thrown an exception.");
} catch (DeviceNotAvailableException expected) {
// expected
@@ -310,7 +317,7 @@
verifyMocks(mMockBuildInfo);
}
- /** Test {@link RemoteAndroidVirtualDevice#postInvocationTearDown()}. */
+ /** Test {@link RemoteAndroidVirtualDevice#postInvocationTearDown(Throwable)}. */
@Test
public void testPostInvocationTearDown() throws Exception {
mTestDevice.setTestLogger(mTestLogger);
@@ -327,7 +334,7 @@
// Initial serial is not set because we call postInvoc directly.
replayMocks();
- mTestDevice.postInvocationTearDown();
+ mTestDevice.postInvocationTearDown(null);
verifyMocks();
Mockito.verify(mGceSshMonitor).shutdown();
Mockito.verify(mGceSshMonitor).joinMonitor();
@@ -418,7 +425,7 @@
EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
EasyMock.expect(mMockBuildInfo.getBuildFlavor()).andStubReturn("flavor");
EasyMock.expect(mMockBuildInfo.getBuildId()).andStubReturn("id");
- TestableRemoteAndroidVirtualDevice testDevice =
+ mTestDevice =
new TestableRemoteAndroidVirtualDevice() {
@Override
public IDevice getIDevice() {
@@ -450,10 +457,10 @@
return mockRunUtil;
}
};
- testDevice.setTestLogger(mTestLogger);
+ mTestDevice.setTestLogger(mTestLogger);
File tmpKeyFile = FileUtil.createTempFile("test-gce", "key");
try {
- OptionSetter setter = new OptionSetter(testDevice.getOptions());
+ OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
setter.setOptionValue("gce-private-key-path", tmpKeyFile.getAbsolutePath());
// We use a missing ssh to prevent the real tunnel from running.
FileUtil.deleteFile(tmpKeyFile);
@@ -489,22 +496,22 @@
replayMocks(mMockBuildInfo);
// Run device a first time
- testDevice.preInvocationSetup(mMockBuildInfo);
- testDevice.getGceSshMonitor().joinMonitor();
+ mTestDevice.preInvocationSetup(mMockBuildInfo);
+ mTestDevice.getGceSshMonitor().joinMonitor();
// We expect to find our Runtime exception for the ssh key
- assertNotNull(testDevice.getGceSshMonitor().getLastException());
- testDevice.postInvocationTearDown();
+ assertNotNull(mTestDevice.getGceSshMonitor().getLastException());
+ mTestDevice.postInvocationTearDown(null);
// Bridge is set to null after tear down
- assertNull(testDevice.getGceSshMonitor());
+ assertNull(mTestDevice.getGceSshMonitor());
// run a second time on same device should yield exact same exception.
- testDevice.preInvocationSetup(mMockBuildInfo);
- testDevice.getGceSshMonitor().joinMonitor();
+ mTestDevice.preInvocationSetup(mMockBuildInfo);
+ mTestDevice.getGceSshMonitor().joinMonitor();
// Should have the same result, the run time exception from ssh key
- assertNotNull(testDevice.getGceSshMonitor().getLastException());
- testDevice.postInvocationTearDown();
+ assertNotNull(mTestDevice.getGceSshMonitor().getLastException());
+ mTestDevice.postInvocationTearDown(null);
// Bridge is set to null after tear down
- assertNull(testDevice.getGceSshMonitor());
+ assertNull(mTestDevice.getGceSshMonitor());
verifyMocks(mMockBuildInfo);
} finally {
@@ -521,7 +528,7 @@
EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
EasyMock.expect(mMockBuildInfo.getBuildFlavor()).andStubReturn("flavor");
EasyMock.expect(mMockBuildInfo.getBuildId()).andStubReturn("id");
- TestableRemoteAndroidVirtualDevice testDevice =
+ mTestDevice =
new TestableRemoteAndroidVirtualDevice() {
@Override
public IDevice getIDevice() {
@@ -553,10 +560,10 @@
return mockRunUtil;
}
};
- testDevice.setTestLogger(mTestLogger);
+ mTestDevice.setTestLogger(mTestLogger);
File tmpKeyFile = FileUtil.createTempFile("test-gce", "key");
try {
- OptionSetter setter = new OptionSetter(testDevice.getOptions());
+ OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
setter.setOptionValue("gce-private-key-path", tmpKeyFile.getAbsolutePath());
// We use a missing ssh to prevent the real tunnel from running.
FileUtil.deleteFile(tmpKeyFile);
@@ -586,11 +593,11 @@
replayMocks(mMockBuildInfo);
// Run device a first time
- testDevice.preInvocationSetup(mMockBuildInfo);
- testDevice.getGceSshMonitor().joinMonitor();
+ mTestDevice.preInvocationSetup(mMockBuildInfo);
+ mTestDevice.getGceSshMonitor().joinMonitor();
// We expect to find our Runtime exception for the ssh key
- assertNotNull(testDevice.getGceSshMonitor().getLastException());
- testDevice.postInvocationTearDown();
+ assertNotNull(mTestDevice.getGceSshMonitor().getLastException());
+ mTestDevice.postInvocationTearDown(null);
// shutdown was disabled, it should not have been called.
verify(mGceHandler, never()).shutdownGce();
verifyMocks(mMockBuildInfo);
@@ -608,7 +615,7 @@
EasyMock.expect(mMockBuildInfo.getBuildBranch()).andStubReturn("branch");
EasyMock.expect(mMockBuildInfo.getBuildFlavor()).andStubReturn("flavor");
EasyMock.expect(mMockBuildInfo.getBuildId()).andStubReturn("id");
- TestableRemoteAndroidVirtualDevice testDevice =
+ mTestDevice =
new TestableRemoteAndroidVirtualDevice() {
@Override
public IDevice getIDevice() {
@@ -640,10 +647,10 @@
return mockRunUtil;
}
};
- testDevice.setTestLogger(mTestLogger);
+ mTestDevice.setTestLogger(mTestLogger);
File tmpKeyFile = FileUtil.createTempFile("test-gce", "key");
try {
- OptionSetter setter = new OptionSetter(testDevice.getOptions());
+ OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
setter.setOptionValue("gce-private-key-path", tmpKeyFile.getAbsolutePath());
// We use a missing ssh to prevent the real tunnel from running.
FileUtil.deleteFile(tmpKeyFile);
@@ -692,18 +699,48 @@
replayMocks(mMockBuildInfo);
// Run device a first time
try {
- testDevice.preInvocationSetup(mMockBuildInfo);
+ mTestDevice.preInvocationSetup(mMockBuildInfo);
fail("Should have thrown an exception.");
} catch (DeviceNotAvailableException expected) {
assertEquals("AVD device booted but was in OFFLINE state", expected.getMessage());
}
- testDevice.getGceSshMonitor().joinMonitor();
+ mTestDevice.getGceSshMonitor().joinMonitor();
// We expect to find our Runtime exception for the ssh key
- assertNotNull(testDevice.getGceSshMonitor().getLastException());
- testDevice.postInvocationTearDown();
+ assertNotNull(mTestDevice.getGceSshMonitor().getLastException());
+ mTestDevice.postInvocationTearDown(null);
verifyMocks(mMockBuildInfo);
} finally {
FileUtil.deleteFile(tmpKeyFile);
}
}
+
+ @Test
+ public void testGetRemoteTombstone() throws Exception {
+ mTestDevice =
+ new TestableRemoteAndroidVirtualDevice() {
+ @Override
+ boolean fetchRemoteDir(File localDir, String remotePath) {
+ try {
+ FileUtil.createTempFile("tombstone_00", "", localDir);
+ FileUtil.createTempFile("tombstone_01", "", localDir);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return true;
+ }
+ };
+ OptionSetter setter = new OptionSetter(mTestDevice.getOptions());
+ setter.setOptionValue(TestDeviceOptions.INSTANCE_TYPE_OPTION, "CUTTLEFISH");
+
+ replayMocks();
+ List<File> tombstones = mTestDevice.getTombstones();
+ try {
+ assertEquals(2, tombstones.size());
+ } finally {
+ for (File f : tombstones) {
+ FileUtil.deleteFile(f);
+ }
+ }
+ verifyMocks();
+ }
}
diff --git a/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
index dd6bde5..0689106 100644
--- a/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/BaseDeviceMetricCollectorTest.java
@@ -17,6 +17,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.times;
import com.android.tradefed.config.OptionSetter;
@@ -104,6 +105,18 @@
Assert.assertEquals(0, mBase.getBuildInfos().size());
}
+ /** Test that multiple call to init are rejected. */
+ @Test
+ public void testMultiInit() {
+ mBase.init(mContext, mMockListener);
+ try {
+ mBase.init(mContext, mMockListener);
+ fail("Should have thrown an exception.");
+ } catch (IllegalStateException expected) {
+ // Expected
+ }
+ }
+
/**
* Test to ensure that the forwarding of events continues even if an exception occurs in the
* collection.
@@ -424,7 +437,7 @@
Mockito.verify(mMockListener, times(1))
.testEnded(Mockito.eq(test3), Mockito.anyLong(), mCapturedMetrics.capture());
Mockito.verify(mMockListener, times(1))
- .testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
+ .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
List<HashMap<String, Metric>> allValues = mCapturedMetrics.getAllValues();
// For test1
@@ -472,7 +485,7 @@
Mockito.verify(mMockListener, times(1))
.testEnded(Mockito.eq(test3), Mockito.anyLong(), mCapturedMetrics.capture());
Mockito.verify(mMockListener, times(1))
- .testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
+ .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
List<HashMap<String, Metric>> allValues = mCapturedMetrics.getAllValues();
// For test1
@@ -485,4 +498,49 @@
assertTrue(allValues.get(2).containsKey("onteststart"));
assertTrue(allValues.get(2).containsKey("ontestend"));
}
+
+ /**
+ * Test that onTestEnd with TestDescription formal supercedes the method signature without a
+ * TestDescription.
+ */
+ @Test
+ public void testOnTestEndWithTestDescription() throws Exception {
+ mBase =
+ new TwoMetricsBaseCollector() {
+ @Override
+ public void onTestEnd(
+ DeviceMetricData testData,
+ final Map<String, Metric> currentTestCaseMetrics,
+ TestDescription test) {
+ testData.addMetric(
+ test.getTestName(),
+ Metric.newBuilder()
+ .setMeasurements(
+ Measurements.newBuilder()
+ .setSingleString("value1")));
+ }
+ };
+ mBase.init(mContext, mMockListener);
+ mBase.invocationStarted(mContext);
+ mBase.testRunStarted("testRun", 1);
+ TestDescription test = new TestDescription("class", "method");
+ mBase.testStarted(test);
+ mBase.testEnded(test, new HashMap<String, Metric>());
+ mBase.testRunEnded(0L, new HashMap<String, Metric>());
+ mBase.invocationEnded(0L);
+
+ Mockito.verify(mMockListener, times(1)).invocationStarted(Mockito.any());
+ Mockito.verify(mMockListener, times(1)).testRunStarted("testRun", 1);
+ Mockito.verify(mMockListener, times(1)).testStarted(Mockito.eq(test), Mockito.anyLong());
+ Mockito.verify(mMockListener, times(1))
+ .testEnded(Mockito.eq(test), Mockito.anyLong(), mCapturedMetrics.capture());
+
+ Mockito.verify(mMockListener, times(1))
+ .testRunEnded(Mockito.anyLong(), (HashMap<String, Metric>) Mockito.any());
+
+ List<HashMap<String, Metric>> allValues = mCapturedMetrics.getAllValues();
+ assertTrue(allValues.get(0).containsKey("onteststart"));
+ assertTrue(allValues.get(0).containsKey("method"));
+ assertTrue(!allValues.get(0).containsKey("ontestend"));
+ }
}
diff --git a/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
index fa40866..d213104 100644
--- a/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/LogcatOnFailureCollectorTest.java
@@ -18,9 +18,11 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import com.android.ddmlib.IDevice;
import com.android.tradefed.config.ConfigurationDef;
import com.android.tradefed.device.ILogcatReceiver;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.NullDevice;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
@@ -44,6 +46,7 @@
private TestableLogcatOnFailureCollector mCollector;
private ITestInvocationListener mMockListener;
private ITestDevice mMockDevice;
+ private ITestDevice mNullMockDevice;
private ITestInvocationListener mTestListener;
private IInvocationContext mContext;
@@ -81,14 +84,19 @@
@Before
public void setUp() {
mMockDevice = EasyMock.createMock(ITestDevice.class);
+ mNullMockDevice = EasyMock.createMock(ITestDevice.class);
mMockListener = EasyMock.createMock(ITestInvocationListener.class);
mMockReceiver = EasyMock.createMock(ILogcatReceiver.class);
mMockRunUtil = EasyMock.createMock(IRunUtil.class);
mCollector = new TestableLogcatOnFailureCollector();
mContext = new InvocationContext();
mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
+ mContext.addAllocatedDevice("second_null_device", mNullMockDevice);
EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("serial");
+ EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
+
+ EasyMock.expect(mNullMockDevice.getIDevice()).andStubReturn(new NullDevice("null-dev"));
}
@Test
@@ -116,14 +124,14 @@
EasyMock.eq(LogDataType.LOGCAT),
EasyMock.anyObject());
- EasyMock.replay(mMockListener, mMockDevice, mMockReceiver);
+ EasyMock.replay(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
mTestListener = mCollector.init(mContext, mMockListener);
mTestListener.testRunStarted("runName", 1);
mTestListener.testStarted(test);
mTestListener.testFailed(test, "I failed");
mTestListener.testEnded(test, new HashMap<String, Metric>());
mTestListener.testRunEnded(0L, new HashMap<String, Metric>());
- EasyMock.verify(mMockListener, mMockDevice, mMockReceiver);
+ EasyMock.verify(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
// Ensure the callback went through
assertTrue(mCollector.mOnTestStartCalled);
assertTrue(mCollector.mOnTestFailCalled);
@@ -132,9 +140,9 @@
@Test
public void testCollect_noRuns() throws Exception {
// If there was no runs, nothing should be done.
- EasyMock.replay(mMockListener, mMockDevice, mMockReceiver);
+ EasyMock.replay(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
mTestListener = mCollector.init(mContext, mMockListener);
- EasyMock.verify(mMockListener, mMockDevice, mMockReceiver);
+ EasyMock.verify(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
assertFalse(mCollector.mOnTestStartCalled);
assertFalse(mCollector.mOnTestFailCalled);
}
@@ -187,7 +195,7 @@
EasyMock.eq(LogDataType.LOGCAT),
EasyMock.anyObject());
- EasyMock.replay(mMockListener, mMockDevice, mMockReceiver);
+ EasyMock.replay(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
mTestListener = mCollector.init(mContext, mMockListener);
mTestListener.testRunStarted("runName", 1);
mTestListener.testStarted(test);
@@ -200,7 +208,7 @@
mTestListener.testFailed(test2, "I failed");
mTestListener.testEnded(test2, new HashMap<String, Metric>());
mTestListener.testRunEnded(0L, new HashMap<String, Metric>());
- EasyMock.verify(mMockListener, mMockDevice, mMockReceiver);
+ EasyMock.verify(mMockListener, mMockDevice, mMockReceiver, mNullMockDevice);
// Ensure the callback went through
assertTrue(mCollector.mOnTestStartCalled);
assertTrue(mCollector.mOnTestFailCalled);
diff --git a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
index 485f295..3aabd79 100644
--- a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
@@ -16,6 +16,8 @@
package com.android.tradefed.device.metric;
+import static org.junit.Assert.assertTrue;
+
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.IInvocationContext;
@@ -69,7 +71,6 @@
OptionSetter setter = new OptionSetter(mPerfettoMetricCollector);
setter.setOptionValue("pull-pattern-keys", "perfettofile");
HashMap<String, Metric> currentMetrics = new HashMap<>();
- currentMetrics.put("perfettofile", TfMetricProtoUtil.stringToMetric("/data/trace.pb"));
Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/trace.pb")))
.thenReturn(new File("trace"));
@@ -78,8 +79,8 @@
mPerfettoMetricCollector.testStarted(testDesc);
mPerfettoMetricCollector.testEnded(testDesc, currentMetrics);
- Mockito.verify(mMockListener)
- .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+ assertTrue("Trace duration available but not expected.",
+ currentMetrics.size() == 0);
}
@Test
@@ -106,6 +107,44 @@
Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.any());
Mockito.verify(mMockListener)
.testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+ assertTrue("Expected two metrics that includes success status",
+ currentMetrics.get("perfetto_trace_extractor_status").getMeasurements()
+ .getSingleString().equals("1"));
+ assertTrue("Trace duration metrics not available but expected.",
+ currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
+ .getSingleDouble() >= 0);
+ }
+
+ @Test
+ public void testScriptFailureStatus() throws Exception {
+
+ OptionSetter setter = new OptionSetter(mPerfettoMetricCollector);
+ setter.setOptionValue("pull-pattern-keys", "perfettofile");
+ setter.setOptionValue("perfetto-binary-path", "trx");
+ HashMap<String, Metric> currentMetrics = new HashMap<>();
+ currentMetrics.put("perfettofile", TfMetricProtoUtil.stringToMetric("/data/trace.pb"));
+ Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/trace.pb")))
+ .thenReturn(new File("trace"));
+
+ TestDescription testDesc = new TestDescription("xyz", "abc");
+ CommandResult cr = new CommandResult();
+ cr.setStatus(CommandStatus.FAILED);
+ cr.setStdout("abc:efg");
+
+ Mockito.doReturn(cr).when(mPerfettoMetricCollector).runHostCommand(Mockito.any());
+
+ mPerfettoMetricCollector.testStarted(testDesc);
+ mPerfettoMetricCollector.testEnded(testDesc, currentMetrics);
+
+ Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.any());
+ Mockito.verify(mMockListener)
+ .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+ assertTrue("Expected two metrics that includes failure status",
+ currentMetrics.get("perfetto_trace_extractor_status").getMeasurements()
+ .getSingleString().equals("0"));
+ assertTrue("Trace duration metrics not available but expected.",
+ currentMetrics.get("perfetto_trace_extractor_runtime").getMeasurements()
+ .getSingleDouble() >= 0);
}
@Test
diff --git a/tests/src/com/android/tradefed/device/metric/RebootReasonCollectorTest.java b/tests/src/com/android/tradefed/device/metric/RebootReasonCollectorTest.java
index da81ba0..34aed61 100644
--- a/tests/src/com/android/tradefed/device/metric/RebootReasonCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/RebootReasonCollectorTest.java
@@ -144,8 +144,21 @@
.getMeasurements()
.getSingleString())
.equals(1));
+ boolean totalCountCorrect =
+ runMetrics
+ .entrySet()
+ .stream()
+ .anyMatch(
+ entry ->
+ entry.getKey().equals(RebootReasonCollector.COUNT_KEY)
+ && Integer.valueOf(
+ entry.getValue()
+ .getMeasurements()
+ .getSingleString())
+ .equals(3));
Assert.assertTrue(reason1CountCorrect);
Assert.assertTrue(reason2CountCorrect);
+ Assert.assertTrue(totalCountCorrect);
}
/** Test that the collector makes the correct callbacks when testing multiple devices. */
@@ -184,6 +197,8 @@
ITestDevice testDevice1 = mockTestDevice(DEVICE_SERIAL_1);
ITestDevice testDevice2 = mockTestDevice(DEVICE_SERIAL_2);
doReturn(Arrays.asList(testDevice1, testDevice2)).when(mContext).getDevices();
+ doReturn(DEVICE_SERIAL_1).when(mContext).getDeviceName(testDevice1);
+ doReturn(DEVICE_SERIAL_2).when(mContext).getDeviceName(testDevice2);
doReturn(CONFIG_ID_1).when(mCollector).pushStatsConfig(eq(testDevice1), any(List.class));
doReturn(CONFIG_ID_2).when(mCollector).pushStatsConfig(eq(testDevice2), any(List.class));
doReturn(Arrays.asList(mockBootEventMetric("bootloader_reason", "system_reason")))
@@ -214,6 +229,19 @@
.getMeasurements()
.getSingleString())
.equals(1));
+ boolean device1TotalCountCorrect =
+ runMetrics
+ .entrySet()
+ .stream()
+ .anyMatch(
+ entry ->
+ entry.getKey().contains(RebootReasonCollector.COUNT_KEY)
+ && entry.getKey().contains(DEVICE_SERIAL_1)
+ && Integer.valueOf(
+ entry.getValue()
+ .getMeasurements()
+ .getSingleString())
+ .equals(1));
boolean device2CountCorrect =
runMetrics
.entrySet()
@@ -223,19 +251,34 @@
entry.getKey().contains(RebootReasonCollector.METRIC_PREFIX)
&& entry.getKey().contains("bootloader_reason")
&& entry.getKey().contains("system_reason")
- && entry.getKey().contains(DEVICE_SERIAL_1)
+ && entry.getKey().contains(DEVICE_SERIAL_2)
+ && Integer.valueOf(
+ entry.getValue()
+ .getMeasurements()
+ .getSingleString())
+ .equals(1));
+ boolean device2TotalCountCorrect =
+ runMetrics
+ .entrySet()
+ .stream()
+ .anyMatch(
+ entry ->
+ entry.getKey().contains(RebootReasonCollector.COUNT_KEY)
+ && entry.getKey().contains(DEVICE_SERIAL_2)
&& Integer.valueOf(
entry.getValue()
.getMeasurements()
.getSingleString())
.equals(1));
Assert.assertTrue(device1CountCorrect);
+ Assert.assertTrue(device1TotalCountCorrect);
Assert.assertTrue(device2CountCorrect);
+ Assert.assertTrue(device2TotalCountCorrect);
}
- /** Test that no metrics are added when no metrics are received. */
+ /** Test that only a count is added when no reboots were recorded. */
@Test
- public void testNoMetrics() throws Exception {
+ public void testCountOnlyWhenNoReboots() throws Exception {
ITestDevice testDevice = mockTestDevice(DEVICE_SERIAL_1);
when(mContext.getDevices()).thenReturn(Arrays.asList(testDevice));
doReturn(CONFIG_ID_1)
@@ -255,6 +298,11 @@
.keySet()
.stream()
.noneMatch(key -> key.startsWith(RebootReasonCollector.METRIC_PREFIX)));
+ Assert.assertTrue(
+ runMetrics
+ .keySet()
+ .stream()
+ .anyMatch(key -> key.contains(RebootReasonCollector.COUNT_KEY)));
}
private ITestDevice mockTestDevice(String serial) {
diff --git a/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java b/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
index 06092b7..683a466 100644
--- a/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/ScreenshotOnFailureCollectorTest.java
@@ -15,6 +15,7 @@
*/
package com.android.tradefed.device.metric;
+import com.android.ddmlib.IDevice;
import com.android.tradefed.config.ConfigurationDef;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.IInvocationContext;
@@ -50,7 +51,7 @@
mCollector = new ScreenshotOnFailureCollector();
mContext = new InvocationContext();
mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
- mTestListener = mCollector.init(mContext, mMockListener);
+ EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
EasyMock.expect(mMockDevice.getSerialNumber()).andReturn("serial");
}
@@ -72,6 +73,7 @@
EasyMock.anyObject());
EasyMock.replay(mMockListener, mMockDevice);
+ mTestListener = mCollector.init(mContext, mMockListener);
mTestListener.testStarted(test);
mTestListener.testFailed(test, "I failed");
mTestListener.testEnded(test, new HashMap<String, Metric>());
diff --git a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
index 5384a65..8623025 100644
--- a/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/SandboxedInvocationExecutionTest.java
@@ -35,6 +35,7 @@
import com.android.tradefed.guice.InvocationScope;
import com.android.tradefed.invoker.sandbox.SandboxedInvocationExecution;
import com.android.tradefed.log.ILogRegistry;
+import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.ILogSaver;
import com.android.tradefed.result.ITestInvocationListener;
@@ -179,7 +180,8 @@
public boolean shardConfig(
IConfiguration config,
IInvocationContext context,
- IRescheduler rescheduler) {
+ IRescheduler rescheduler,
+ ITestLogger logger) {
// Ensure that sharding is not called against a sandbox
// configuration run
throw new RuntimeException("Should not be called.");
@@ -308,7 +310,7 @@
// Device early preInvocationSetup was called and even if no tests run we still call tear
// down
Mockito.verify(mMockDevice).preInvocationSetup(any(), any());
- Mockito.verify(mMockDevice).postInvocationTearDown();
+ Mockito.verify(mMockDevice).postInvocationTearDown(null);
}
/**
@@ -378,6 +380,6 @@
// Device early preInvocationSetup was called and even if no tests run we still call tear
// down
Mockito.verify(mMockDevice).preInvocationSetup(any(), any());
- Mockito.verify(mMockDevice).postInvocationTearDown();
+ Mockito.verify(mMockDevice).postInvocationTearDown(exception);
}
}
diff --git a/tests/src/com/android/tradefed/invoker/ShardListenerTest.java b/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
index c835038..a8127b1 100644
--- a/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
+++ b/tests/src/com/android/tradefed/invoker/ShardListenerTest.java
@@ -78,6 +78,27 @@
EasyMock.verify(mMockListener, mMockDevice);
}
+ /** Test that we can replay events even if invocationEnded hasn't be called yet. */
+ @Test
+ public void testPlayRuns() {
+ mMockListener.invocationStarted(mContext);
+ mMockListener.testRunStarted("run1", 1);
+ TestDescription tid = new TestDescription("class1", "name1");
+ mMockListener.testStarted(tid, 0l);
+ mMockListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+ mMockListener.testRunEnded(0l, new HashMap<String, Metric>());
+ // mMockListener.invocationEnded(0l); On purpose not calling invocationEnded.
+
+ EasyMock.replay(mMockListener, mMockDevice);
+ mShardListener.invocationStarted(mContext);
+ mShardListener.testRunStarted("run1", 1);
+ mShardListener.testStarted(tid, 0l);
+ mShardListener.testEnded(tid, 0l, new HashMap<String, Metric>());
+ mShardListener.testRunEnded(0l, new HashMap<String, Metric>());
+ // mShardListener.invocationEnded(0l); On purpose not calling invocationEnded.
+ EasyMock.verify(mMockListener, mMockDevice);
+ }
+
/** Ensure that replaying a log without a run (no tests ran) still works. */
@Test
public void testLogWithoutRun() {
@@ -195,11 +216,43 @@
// Log association to re-associate file to the run.
mockListener.logAssociation("run-file", runFile);
mockListener.testRunEnded(0l, new HashMap<String, Metric>());
+
+ // The log not associated to the run are replay at invocation level.
+ mockListener.testLog(
+ EasyMock.eq("host_log_of_shard"),
+ EasyMock.eq(LogDataType.TEXT),
+ EasyMock.anyObject());
+ LogFile invocFile = new LogFile("path", "url", false, LogDataType.TEXT, 0L);
+ EasyMock.expect(
+ mMockSaver.saveLogData(
+ EasyMock.eq("host_log_of_shard"),
+ EasyMock.eq(LogDataType.TEXT),
+ EasyMock.anyObject()))
+ .andReturn(invocFile);
+ mockListener.testLogSaved(
+ EasyMock.eq("host_log_of_shard"),
+ EasyMock.eq(LogDataType.TEXT),
+ EasyMock.anyObject(),
+ EasyMock.eq(invocFile));
+ mockListener.logAssociation("host_log_of_shard", invocFile);
mockListener.invocationEnded(0l);
EasyMock.expect(mockListener.getSummary()).andReturn(null);
+ // TODO: Fix the name of end_host_log for each shard
+ EasyMock.expect(
+ mMockSaver.saveLogData(
+ EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+ EasyMock.eq(LogDataType.TEXT),
+ EasyMock.anyObject()))
+ .andReturn(invocFile);
mMockSaver.invocationEnded(0L);
- EasyMock.expectLastCall().times(2);
+ EasyMock.expect(
+ mMockSaver.saveLogData(
+ EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+ EasyMock.eq(LogDataType.TEXT),
+ EasyMock.anyObject()))
+ .andReturn(invocFile);
+ mMockSaver.invocationEnded(0L);
EasyMock.replay(mockListener, mMockSaver, mMockDevice);
// Setup of sharding
@@ -223,6 +276,10 @@
new ByteArrayInputStreamSource("test file".getBytes()));
shardedInvocation.testEnded(tid, 0l, new HashMap<String, Metric>());
shardedInvocation.testRunEnded(0l, new HashMap<String, Metric>());
+ shardedInvocation.testLog(
+ "host_log_of_shard",
+ LogDataType.TEXT,
+ new ByteArrayInputStreamSource("test".getBytes()));
shardedInvocation.invocationEnded(0L);
EasyMock.verify(mockListener, mMockSaver, mMockDevice);
diff --git a/tests/src/com/android/tradefed/invoker/ShardMasterResultForwarderTest.java b/tests/src/com/android/tradefed/invoker/ShardMasterResultForwarderTest.java
index d85671c..b62dd38 100644
--- a/tests/src/com/android/tradefed/invoker/ShardMasterResultForwarderTest.java
+++ b/tests/src/com/android/tradefed/invoker/ShardMasterResultForwarderTest.java
@@ -168,7 +168,7 @@
invocationLogger.invocationEnded(500L);
// Log saver only saved the file once.
- Mockito.verify(mMockLogSaver, times(1))
+ Mockito.verify(mMockLogSaver, times(2))
.saveLogData(Mockito.any(), Mockito.any(), Mockito.any());
Mockito.verify(mMockLogListener, times(1)).invocationStarted(Mockito.eq(main));
Mockito.verify(mMockLogListener, times(1))
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index 89e6887..c9a22e5 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -170,6 +170,7 @@
EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
EasyMock.expect(mMockConfig.getTests()).andStubReturn(new ArrayList<>());
+ mMockConfig.resolveDynamicOptions();
mMockConfig.cleanDynamicOptionFiles();
IBuildInfo build1 = new BuildInfo();
EasyMock.expect(mProvider1.getBuild()).andReturn(build1);
@@ -190,6 +191,12 @@
mMockLogSaver.saveLogData(
EasyMock.anyObject(), EasyMock.anyObject(), EasyMock.anyObject()))
.andReturn(new LogFile("", "", LogDataType.TEXT));
+ EasyMock.expect(
+ mMockLogSaver.saveLogData(
+ EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
+ .andReturn(new LogFile("", "", LogDataType.TEXT));
mMockTestListener.invocationEnded(EasyMock.anyLong());
EasyMock.expect(mMockTestListener.getSummary()).andReturn(null);
mMockLogSaver.invocationEnded(EasyMock.anyLong());
@@ -250,6 +257,7 @@
EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
EasyMock.expect(mMockConfig.getTests()).andStubReturn(new ArrayList<>());
+ mMockConfig.resolveDynamicOptions();
mMockConfig.cleanDynamicOptionFiles();
mMockTestListener.invocationStarted(mContext);
@@ -261,6 +269,12 @@
mMockLogSaver.saveLogData(
EasyMock.anyObject(), EasyMock.anyObject(), EasyMock.anyObject()))
.andReturn(new LogFile("", "", LogDataType.TEXT));
+ EasyMock.expect(
+ mMockLogSaver.saveLogData(
+ EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
+ .andReturn(new LogFile("", "", LogDataType.TEXT));
mMockTestListener.invocationEnded(EasyMock.anyLong());
EasyMock.expect(mMockTestListener.getSummary()).andReturn(null);
mMockLogSaver.invocationEnded(EasyMock.anyLong());
@@ -330,6 +344,7 @@
EasyMock.expect(mMockConfig.getCommandLine()).andStubReturn("empty");
EasyMock.expect(mMockConfig.getCommandOptions()).andStubReturn(new CommandOptions());
EasyMock.expect(mMockConfig.getTests()).andStubReturn(new ArrayList<>());
+ mMockConfig.resolveDynamicOptions();
mMockConfig.cleanDynamicOptionFiles();
mMockTestListener.invocationStarted(mContext);
@@ -341,6 +356,12 @@
mMockLogSaver.saveLogData(
EasyMock.anyObject(), EasyMock.anyObject(), EasyMock.anyObject()))
.andReturn(new LogFile("", "", LogDataType.TEXT));
+ EasyMock.expect(
+ mMockLogSaver.saveLogData(
+ EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+ EasyMock.anyObject(),
+ EasyMock.anyObject()))
+ .andReturn(new LogFile("", "", LogDataType.TEXT));
mMockTestListener.invocationEnded(EasyMock.anyLong());
EasyMock.expect(mMockTestListener.getSummary()).andReturn(null);
mMockLogSaver.invocationEnded(EasyMock.anyLong());
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index c6ea271..38bca14 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -46,7 +46,6 @@
import com.android.tradefed.device.DeviceAllocationState;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IDeviceRecovery;
-import com.android.tradefed.device.INativeDevice;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.device.StubDevice;
@@ -93,10 +92,10 @@
import org.easymock.Capture;
import org.easymock.EasyMock;
import org.junit.Before;
+import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
-import org.mockito.Mockito;
import java.io.File;
import java.io.IOException;
@@ -155,6 +154,15 @@
private IRescheduler mockRescheduler;
private DeviceDescriptor mFakeDescriptor;
+ @BeforeClass
+ public static void setUpClass() throws Exception {
+ try {
+ GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
+ } catch (IllegalStateException e) {
+ // Avoid exception in case of multi-init
+ }
+ }
+
@Before
public void setUp() throws Exception {
@@ -212,10 +220,16 @@
mMockDevice.preInvocationSetup(
(IBuildInfo) EasyMock.anyObject(), EasyMock.<List<IBuildInfo>>anyObject());
EasyMock.expectLastCall().anyTimes();
- mMockDevice.postInvocationTearDown();
- EasyMock.expectLastCall().anyTimes();
- mFakeDescriptor = new DeviceDescriptor(SERIAL, false, DeviceAllocationState.Available,
- "unknown", "unknown", "unknown", "unknown", "unknown");
+ mFakeDescriptor =
+ new DeviceDescriptor(
+ SERIAL,
+ false,
+ DeviceAllocationState.Available,
+ "unknown",
+ "unknown",
+ "unknown",
+ "unknown",
+ "unknown");
EasyMock.expect(mMockDevice.getDeviceDescriptor()).andStubReturn(mFakeDescriptor);
EasyMock.expect(mMockBuildInfo.getBuildId()).andStubReturn("1");
@@ -229,9 +243,13 @@
// always expect logger initialization and cleanup calls
mMockLogRegistry.registerLogger(mMockLogger);
+ EasyMock.expectLastCall().times(2);
mMockLogger.init();
+ EasyMock.expectLastCall().times(2);
mMockLogger.closeLog();
+ EasyMock.expectLastCall().times(2);
mMockLogRegistry.unregisterLogger();
+ EasyMock.expectLastCall().times(2);
mUriCapture = new Capture<List<TestSummary>>();
mStubInvocationMetadata = new InvocationContext();
@@ -351,6 +369,11 @@
setupMockFailureListeners(exception);
setupInvoke();
+ EasyMock.reset(mMockLogger, mMockLogRegistry);
+ mMockLogRegistry.registerLogger(mMockLogger);
+ mMockLogger.init();
+ mMockLogger.closeLog();
+ mMockLogRegistry.unregisterLogger();
IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
CommandOptions cmdOptions = new CommandOptions();
final String expectedTestTag = "TEST_TAG";
@@ -380,6 +403,12 @@
setupInvoke();
setupMockFailureListenersAny(new BuildRetrievalError("No build found to test."), true);
+ EasyMock.reset(mMockLogger, mMockLogRegistry);
+ mMockLogRegistry.registerLogger(mMockLogger);
+ mMockLogger.init();
+ mMockLogger.closeLog();
+ mMockLogRegistry.unregisterLogger();
+
IRemoteTest test = EasyMock.createMock(IRemoteTest.class);
mStubConfiguration.setTest(test);
EasyMock.expect(mMockLogger.getLog()).andReturn(EMPTY_STREAM_SOURCE);
@@ -408,13 +437,20 @@
IRetriableTest test = EasyMock.createMock(IRetriableTest.class);
EasyMock.expect(test.isRetriable()).andReturn(Boolean.TRUE);
+ setupInvoke();
+
+ EasyMock.reset(mMockLogger, mMockLogRegistry);
+ mMockLogRegistry.registerLogger(mMockLogger);
+ mMockLogger.init();
+ mMockLogger.closeLog();
+ mMockLogRegistry.unregisterLogger();
+
EasyMock.expect(mockRescheduler.rescheduleCommand()).andReturn(EasyMock.anyBoolean());
mStubConfiguration.setTest(test);
mStubConfiguration.getCommandOptions().setLoopMode(false);
mMockLogRegistry.dumpToGlobalLog(mMockLogger);
EasyMock.expectLastCall().times(1);
- setupInvoke();
setupMockFailureListenersAny(new BuildRetrievalError("No build found to test."), true);
Capture<IBuildInfo> captured = new Capture<>();
mMockBuildProvider.cleanUp(EasyMock.capture(captured));
@@ -548,6 +584,9 @@
setupMockFailureListeners(exception);
EasyMock.expect(mMockDevice.getBugreport()).andReturn(EMPTY_STREAM_SOURCE);
setupInvokeWithBuild();
+
+ mMockDevice.postInvocationTearDown(exception);
+
replayMocks(test);
EasyMock.replay(mockRescheduler);
mTestInvocation.invoke(mStubInvocationMetadata, mStubConfiguration, mockRescheduler);
@@ -611,6 +650,12 @@
EasyMock.eq(LogDataType.TEXT),
(InputStream) EasyMock.anyObject()))
.andReturn(new LogFile(PATH, URL, LogDataType.TEXT));
+ EasyMock.expect(
+ mMockLogSaver.saveLogData(
+ EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+ EasyMock.eq(LogDataType.TEXT),
+ (InputStream) EasyMock.anyObject()))
+ .andReturn(new LogFile(PATH, URL, LogDataType.TEXT));
resumeListener.testLog(
EasyMock.startsWith(LOGCAT_NAME_SETUP),
EasyMock.eq(LogDataType.LOGCAT),
@@ -704,6 +749,7 @@
EasyMock.expectLastCall().times(3);
mMockDevice.clearLastConnectedWifiNetwork();
mMockDevice.stopLogcat();
+ mMockDevice.postInvocationTearDown(null);
EasyMock.replay(mockRescheduler, resumeListener, resumableTest, mMockPreparer,
mMockBuildProvider, mMockLogger, mMockLogSaver, mMockDevice, mMockBuildInfo);
@@ -736,6 +782,10 @@
IRescheduler mockRescheduler = EasyMock.createMock(IRescheduler.class);
EasyMock.expect(mockRescheduler.rescheduleCommand()).andReturn(EasyMock.anyBoolean());
mMockBuildProvider.buildNotTested(mMockBuildInfo);
+
+ mMockDevice.postInvocationTearDown(exception);
+ EasyMock.expectLastCall().anyTimes();
+
setupMockFailureListeners(exception);
setupNormalInvoke(test);
EasyMock.replay(mockRescheduler);
@@ -1120,6 +1170,11 @@
mMockSummaryListener.invocationStarted(mStubInvocationMetadata);
EasyMock.expect(mMockSummaryListener.getSummary()).andReturn(null);
+ if (throwable == null) {
+ mMockDevice.postInvocationTearDown(null);
+ EasyMock.expectLastCall().anyTimes();
+ }
+
if (!(throwable instanceof BuildRetrievalError)) {
EasyMock.expect(
mMockLogSaver.saveLogData(
@@ -1218,6 +1273,12 @@
EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
mMockSummaryListener.testLog(EasyMock.eq(TestInvocation.TRADEFED_LOG_NAME),
EasyMock.eq(LogDataType.TEXT), (InputStreamSource)EasyMock.anyObject());
+ EasyMock.expect(
+ mMockLogSaver.saveLogData(
+ EasyMock.eq(TestInvocation.TRADEFED_END_HOST_LOG),
+ EasyMock.eq(LogDataType.TEXT),
+ (InputStream) EasyMock.anyObject()))
+ .andReturn(new LogFile(PATH, URL, LogDataType.TEXT));
// invocationEnded, getSummary (mMockTestListener)
mMockTestListener.invocationEnded(EasyMock.anyLong());
@@ -1235,12 +1296,6 @@
*/
@Test
public void testInvoke_shardableTest_legacy() throws Throwable {
- try {
- GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
- } catch (IllegalStateException e) {
- // Avoid exception in case of multi-init
- }
-
String command = "empty --test-tag t";
String[] commandLine = {"empty", "--test-tag", "t"};
int shardCount = 2;
@@ -1259,6 +1314,11 @@
.andReturn(new StubKeyStoreFactory())
.times(2);
setupInvoke();
+ EasyMock.reset(mMockLogger, mMockLogRegistry);
+ mMockLogRegistry.registerLogger(mMockLogger);
+ mMockLogger.init();
+ mMockLogger.closeLog();
+ mMockLogRegistry.unregisterLogger();
mMockLogSaver.invocationStarted(mStubInvocationMetadata);
mMockLogSaver.invocationEnded(0L);
setupNShardInvocation(shardCount, command);
@@ -1488,76 +1548,6 @@
}
/**
- * Test {@link INativeDevice#preInvocationSetup(IBuildInfo, List)} is called when command option
- * skip-pre-device-setup is not set.
- */
- @Test
- public void testNotSkipPreDeviceSetup() throws Throwable {
- IInvocationContext context = new InvocationContext();
- ITestDevice device1 = EasyMock.createMock(ITestDevice.class);
- IDevice idevice = Mockito.mock(IDevice.class);
- context.addAllocatedDevice("DEFAULT_DEVICE", device1);
- IBuildInfo testResourceBuildInfo = new BuildInfo();
- testResourceBuildInfo.setTestResourceBuild(true);
- context.addDeviceBuildInfo("test-resource", testResourceBuildInfo);
- List<IBuildInfo> testResourceBuildInfos = new ArrayList<>();
- testResourceBuildInfos.add(testResourceBuildInfo);
- EasyMock.expect(device1.getSerialNumber()).andReturn("serial1").anyTimes();
- EasyMock.expect(device1.getIDevice()).andReturn(idevice).anyTimes();
-
- device1.preInvocationSetup(
- (IBuildInfo) EasyMock.anyObject(), EasyMock.eq(testResourceBuildInfos));
- EasyMock.expectLastCall().once();
-
- CommandOptions commandOption = new CommandOptions();
- OptionSetter setter = new OptionSetter(commandOption);
- setter.setOptionValue("skip-pre-device-setup", "false");
- mStubConfiguration.setCommandOptions(commandOption);
- // Not expect isTearDownDisabled.
- ITestInvocationListener listener = EasyMock.createStrictMock(ITestInvocationListener.class);
- EasyMock.replay(device1, listener, mMockPreparer);
- new InvocationExecution()
- .runDevicePreInvocationSetup(context, mStubConfiguration, listener);
- EasyMock.verify(device1, listener, mMockPreparer);
-
- }
-
- /**
- * Test {@link INativeDevice#preInvocationSetup(IBuildInfo info)} is not called when command
- * option skip-pre-device-setup is set.
- */
- @Test
- public void testSkipPreDeviceSetup() throws Throwable {
- IInvocationContext context = new InvocationContext();
- ITestDevice device1 = EasyMock.createMock(ITestDevice.class);
- IDevice idevice = Mockito.mock(IDevice.class);
- context.addAllocatedDevice("DEFAULT_DEVICE", device1);
- EasyMock.expect(device1.getSerialNumber()).andReturn("serial1").anyTimes();
- EasyMock.expect(device1.getIDevice()).andReturn(idevice).anyTimes();
- EasyMock.expect(device1.getLogcat()).andReturn(EMPTY_STREAM_SOURCE).times(1);
- device1.clearLogcat();
- EasyMock.expectLastCall().once();
-
- CommandOptions commandOption = new CommandOptions();
- OptionSetter setter = new OptionSetter(commandOption);
- setter.setOptionValue("skip-pre-device-setup", "true");
- mStubConfiguration.setCommandOptions(commandOption);
-
- EasyMock.expect(mMockPreparer.isDisabled()).andReturn(true);
- // Not expect isTearDownDisabled
-
- ITestInvocationListener listener = EasyMock.createStrictMock(ITestInvocationListener.class);
- listener.testLog(
- EasyMock.startsWith(LOGCAT_NAME_SETUP),
- EasyMock.eq(LogDataType.LOGCAT),
- (InputStreamSource) EasyMock.anyObject());
-
- EasyMock.replay(device1, listener, mMockPreparer);
- new InvocationExecution().doSetup(context, mStubConfiguration, listener);
- EasyMock.verify(device1, listener, mMockPreparer);
- }
-
- /**
* Test when a {@link IDeviceBuildInfo} is passing through we do not attempt to add any external
* directories when there is none coming from environment.
*/
diff --git a/tests/src/com/android/tradefed/invoker/logger/InvocationMetricLoggerTest.java b/tests/src/com/android/tradefed/invoker/logger/InvocationMetricLoggerTest.java
new file mode 100644
index 0000000..c7eead0
--- /dev/null
+++ b/tests/src/com/android/tradefed/invoker/logger/InvocationMetricLoggerTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.invoker.logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.UUID;
+
+/** Unit tests for {@link InvocationMetricLogger}. */
+@RunWith(JUnit4.class)
+public class InvocationMetricLoggerTest {
+
+ @Test
+ public void testLogMetrics() throws Exception {
+ Map<String, String> result = logMetric(InvocationMetricKey.FETCH_BUILD, "TEST");
+ assertEquals("TEST", result.get(InvocationMetricKey.FETCH_BUILD.toString()));
+ // Ensure that it wasn't added in current ThreadGroup
+ assertNull(
+ InvocationMetricLogger.getInvocationMetrics()
+ .get(InvocationMetricKey.FETCH_BUILD.toString()));
+ }
+
+ private Map<String, String> logMetric(InvocationMetricKey key, String value) throws Exception {
+ String uuid = UUID.randomUUID().toString();
+ ThreadGroup testGroup = new ThreadGroup("unit-test-group-" + uuid);
+ TestRunnable runnable = new TestRunnable(key, value);
+ Thread testThread = new Thread(testGroup, runnable);
+ testThread.setName("InvocationMetricLoggerTest-test-thread");
+ testThread.setDaemon(true);
+ testThread.start();
+ testThread.join(10000);
+ return runnable.getResultMap();
+ }
+
+ private class TestRunnable implements Runnable {
+
+ private InvocationMetricKey mKey;
+ private String mValue;
+ private Map<String, String> mResultMap;
+
+ public TestRunnable(InvocationMetricKey key, String value) {
+ mKey = key;
+ mValue = value;
+ }
+
+ @Override
+ public void run() {
+ InvocationMetricLogger.addInvocationMetrics(mKey, mValue);
+ mResultMap = InvocationMetricLogger.getInvocationMetrics();
+ }
+
+ public Map<String, String> getResultMap() {
+ return mResultMap;
+ }
+ }
+}
diff --git a/tests/src/com/android/tradefed/invoker/shard/ShardHelperTest.java b/tests/src/com/android/tradefed/invoker/shard/ShardHelperTest.java
index 58d0bac..f4c0e5f 100644
--- a/tests/src/com/android/tradefed/invoker/shard/ShardHelperTest.java
+++ b/tests/src/com/android/tradefed/invoker/shard/ShardHelperTest.java
@@ -135,7 +135,7 @@
setter.setOptionValue("num-shards", "5");
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
- assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Ensure that we did split 1 tests per shard rescheduled.
Mockito.verify(mRescheduler, Mockito.times(3))
.scheduleConfig(
@@ -161,7 +161,7 @@
setter.setOptionValue("num-shards", "5");
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
- assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Ensure that we did split 1 tests per shard rescheduled.
Mockito.verify(mRescheduler, Mockito.times(5))
.scheduleConfig(
@@ -193,7 +193,7 @@
setter.setOptionValue("num-shards", "5");
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
- assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// We only reschedule 5 times and not 10 like --shard-count because there is not enough
// tests to put at least 1 test per shard. So there is no point in rescheduling on new
// devices.
@@ -238,7 +238,7 @@
setter.setOptionValue("num-shards", "5");
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
- assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Ensure that we did split 1 tests per shard rescheduled.
Mockito.verify(mRescheduler, Mockito.times(3))
.scheduleConfig(
@@ -285,7 +285,7 @@
setter.setOptionValue("num-shards", "5");
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
- assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Ensure that we did split 1 tests per shard rescheduled.
Mockito.verify(mRescheduler, Mockito.times(3))
.scheduleConfig(
@@ -359,7 +359,7 @@
doReturn(true).when(mockClient).isAvailable();
doReturn(SuccessTestCase.class.getName()).when(mockClient).fetchKey("test");
- assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Ensure that we did split 1 tests per shard rescheduled.
Mockito.verify(mRescheduler, Mockito.times(2))
.scheduleConfig(
@@ -420,7 +420,7 @@
doReturn(SuccessTestCase.class.getName()).when(mockClient).fetchKey("test");
doThrow(new RuntimeException()).when(mockClient).fetchKey("test");
try {
- mHelper.shardConfig(mConfig, mContext, mRescheduler);
+ mHelper.shardConfig(mConfig, mContext, mRescheduler, null);
fail("Should have thrown an exception.");
} catch (RuntimeException expected) {
// expected
@@ -452,7 +452,7 @@
tests.add(new TokenTestClass());
mConfig.setTests(tests);
assertEquals(2, mConfig.getTests().size());
- assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Ensure that we did split 1 tests per shard rescheduled.
Mockito.verify(mRescheduler, Mockito.times(3))
.scheduleConfig(
diff --git a/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java b/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
index 0f162d4..00d098e 100644
--- a/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
+++ b/tests/src/com/android/tradefed/invoker/shard/StrictShardHelperTest.java
@@ -92,7 +92,7 @@
setter.setOptionValue("num-shards", "5");
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
- assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertTrue(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Ensure that we did split 1 tests per shard rescheduled.
Mockito.verify(mRescheduler, Mockito.times(5))
.scheduleConfig(
@@ -121,7 +121,7 @@
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
// We do not shard, we are relying on the current invocation to run.
- assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Rescheduled is NOT called because we use the current invocation to run the index.
Mockito.verify(mRescheduler, Mockito.times(0)).scheduleConfig(Mockito.any());
assertEquals(1, mConfig.getTests().size());
@@ -152,7 +152,7 @@
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
// We do not shard, we are relying on the current invocation to run.
- assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Rescheduled is NOT called because we use the current invocation to run the index.
Mockito.verify(mRescheduler, Mockito.times(0)).scheduleConfig(Mockito.any());
assertEquals(1, mConfig.getTests().size());
@@ -183,7 +183,7 @@
mConfig.setTest(test);
assertEquals(1, mConfig.getTests().size());
// We do not shard, we are relying on the current invocation to run.
- assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler));
+ assertFalse(mHelper.shardConfig(mConfig, mContext, mRescheduler, null));
// Rescheduled is NOT called because we use the current invocation to run the index.
Mockito.verify(mRescheduler, Mockito.times(0)).scheduleConfig(Mockito.any());
// We have no tests to put in shard-index 1 so it's empty.
@@ -249,7 +249,7 @@
mConfig.setCommandOptions(options);
mConfig.setCommandLine(new String[] {"empty"});
mConfig.setTests(test);
- mHelper.shardConfig(mConfig, mContext, mRescheduler);
+ mHelper.shardConfig(mConfig, mContext, mRescheduler, null);
return mConfig.getTests();
}
@@ -314,7 +314,7 @@
@Test
public void testShardSuite() throws Exception {
//mConfig
- mHelper.shardConfig(mConfig, mContext, mRescheduler);
+ mHelper.shardConfig(mConfig, mContext, mRescheduler, null);
}
/**
@@ -365,7 +365,7 @@
setter.setOptionValue("shard-index", Integer.toString(0));
mConfig.setCommandOptions(options);
mConfig.setTest(test);
- mHelper.shardConfig(mConfig, mContext, mRescheduler);
+ mHelper.shardConfig(mConfig, mContext, mRescheduler, null);
List<IRemoteTest> res = mConfig.getTests();
assertEquals(1, res.size());
diff --git a/tests/src/com/android/tradefed/log/HistoryLoggerTest.java b/tests/src/com/android/tradefed/log/HistoryLoggerTest.java
index 65687d1..dd48251 100644
--- a/tests/src/com/android/tradefed/log/HistoryLoggerTest.java
+++ b/tests/src/com/android/tradefed/log/HistoryLoggerTest.java
@@ -22,7 +22,6 @@
import org.junit.Assert;
import org.junit.Test;
-import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@@ -52,7 +51,7 @@
mLogger =
new HistoryLogger() {
@Override
- void writeToLog(String outMessage) throws IOException {
+ protected void writeToLog(String outMessage) {
Assert.assertEquals("INVOCATION_END: {\"test\":\"value\"}\n", outMessage);
mWasCalled = true;
}
@@ -72,7 +71,7 @@
mLogger =
new HistoryLogger() {
@Override
- void writeToLog(String outMessage) throws IOException {
+ protected void writeToLog(String outMessage) {
Assert.assertEquals("INVOCATION_END: {}\n", outMessage);
mWasCalled = true;
}
diff --git a/tests/src/com/android/tradefed/log/LogRegistryTest.java b/tests/src/com/android/tradefed/log/LogRegistryTest.java
index 5ddba9c..b43ff87 100644
--- a/tests/src/com/android/tradefed/log/LogRegistryTest.java
+++ b/tests/src/com/android/tradefed/log/LogRegistryTest.java
@@ -18,28 +18,23 @@
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
+import junit.framework.TestCase;
+
import org.easymock.EasyMock;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import java.util.List;
-
-/** Unit tests for {@link LogRegistry}. */
-@RunWith(JUnit4.class)
-public class LogRegistryTest {
+/**
+ * Unit tests for {@link LogRegistry}.
+ */
+public class LogRegistryTest extends TestCase {
private static final String LOG_TAG = "LogRegistryTest";
- private static final long MAX_HISTORY_SIZE = 5;
private LogRegistry mLogRegistry;
private ThreadGroup mStubThreadGroup;
- @Before
- public void setUp() throws Exception {
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
mStubThreadGroup = new ThreadGroup("LogRegistryTest");
mLogRegistry =
new LogRegistry() {
@@ -54,29 +49,24 @@
public void saveGlobalLog() {
// empty on purpose, avoid leaving logs that we can't clean.
}
-
- @Override
- long getMaxHistorySize() {
- return MAX_HISTORY_SIZE;
- }
};
}
- @After
- public void tearDown() throws Exception {
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
mLogRegistry.closeAndRemoveAllLogs();
}
/**
* Tests that {@link LogRegistry#getLogger} returns the logger that was previously registered.
*/
- @Test
public void testGetLogger() {
StdoutLogger stdoutLogger = new StdoutLogger();
mLogRegistry.registerLogger(stdoutLogger);
ILeveledLogOutput returnedLogger = mLogRegistry.getLogger();
- Assert.assertEquals(stdoutLogger, returnedLogger);
+ assertEquals(stdoutLogger, returnedLogger);
mLogRegistry.unregisterLogger();
}
@@ -84,7 +74,6 @@
* Tests that {@link LogRegistry#printLog} calls into the underlying logger's printLog method
* when the logging level is appropriate for printing.
*/
- @Test
public void testPrintLog_sameLogLevel() {
String testMessage = "This is a test message.";
ILeveledLogOutput mockLogger = EasyMock.createMock(ILeveledLogOutput.class);
@@ -102,7 +91,6 @@
* Tests that {@link LogRegistry#printLog} does not call into the underlying logger's printLog
* method when the logging level is lower than necessary to log.
*/
- @Test
public void testPrintLog_lowerLogLevel() {
String testMessage = "This is a test message.";
ILeveledLogOutput mockLogger = EasyMock.createMock(ILeveledLogOutput.class);
@@ -117,10 +105,9 @@
}
/**
- * Tests for ensuring new threads spawned without an explicit ThreadGroup will inherit the same
- * logger as the parent's logger.
+ * Tests for ensuring new threads spawned without an explicit ThreadGroup will inherit the
+ * same logger as the parent's logger.
*/
- @Test
public void testThreadedLogging() {
final String testMessage = "Another test message!";
final ILeveledLogOutput mockLogger = EasyMock.createMock(ILeveledLogOutput.class);
@@ -144,7 +131,7 @@
secondThread.join(); // threaded, but force serialization for testing
}
catch (InterruptedException ie) {
- Assert.fail("Thread was unexpectedly interrupted.");
+ fail("Thread was unexpectedly interrupted.");
}
finally {
mLogRegistry.unregisterLogger();
@@ -169,35 +156,7 @@
firstThread.join(); // threaded, but force serialization for testing
}
catch (InterruptedException ie) {
- Assert.fail("Thread was unexpectedly interrupted.");
+ fail("Thread was unexpectedly interrupted.");
}
}
-
- /** Tests {@link LogRegistry#getLogEntriesAfter}. */
- @Test
- public void testGetLogEntriesAfter() {
- mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, "message1");
- mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, "message2");
- List<LogEntry> logs = mLogRegistry.getLogEntriesAfter(null);
- Assert.assertEquals(2, logs.size());
- Assert.assertEquals("message1", logs.get(0).getMessage());
- Assert.assertEquals("message2", logs.get(1).getMessage());
- mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, "message3");
- mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, "message4");
- logs = mLogRegistry.getLogEntriesAfter(logs.get(1));
- Assert.assertEquals(2, logs.size());
- Assert.assertEquals("message3", logs.get(0).getMessage());
- Assert.assertEquals("message4", logs.get(1).getMessage());
- }
-
- /** Tests {@link LogRegistry#getLogEntriesAfter} will not store more than limit. */
- @Test
- public void testGetLogEntriesAfter_exceedLimit() {
- for (int i = 0; i < MAX_HISTORY_SIZE * 2; ++i) {
- mLogRegistry.printLog(LogLevel.VERBOSE, LOG_TAG, String.format("message-%d", i));
- }
- List<LogEntry> logs = mLogRegistry.getLogEntriesAfter(null);
- Assert.assertEquals(MAX_HISTORY_SIZE, logs.size());
- Assert.assertEquals("message-9", logs.get(logs.size() - 1).getMessage());
- }
}
diff --git a/tests/src/com/android/tradefed/log/LogUtilFuncTest.java b/tests/src/com/android/tradefed/log/LogUtilFuncTest.java
index 136d23c..44d3a75 100644
--- a/tests/src/com/android/tradefed/log/LogUtilFuncTest.java
+++ b/tests/src/com/android/tradefed/log/LogUtilFuncTest.java
@@ -18,14 +18,10 @@
import com.android.ddmlib.Log;
import com.android.ddmlib.Log.LogLevel;
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.config.IGlobalConfiguration;
import com.android.tradefed.log.LogUtil.CLog;
import junit.framework.TestCase;
-import org.easymock.EasyMock;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -36,14 +32,6 @@
public class LogUtilFuncTest extends TestCase {
private static final String CLASS_NAME = "LogUtilFuncTest";
private static final String STRING = "hallo!";
- private ITerribleFailureHandler mMockWtfHandler;
- private IGlobalConfiguration mMockGlobalConfig;
-
- @Override
- protected void setUp() throws Exception, ConfigurationException {
- mMockWtfHandler = EasyMock.createMock(ITerribleFailureHandler.class);
- mMockGlobalConfig = EasyMock.createMock(IGlobalConfiguration.class);
- }
public void testCLog_v() {
Log.v(CLASS_NAME, "this is the real Log.v");
@@ -76,47 +64,6 @@
}
/**
- * Verify that all variants of calling CLog.wtf() results in a wtf handler being called
- */
- public void testCLog_wtf() {
- // force CLog.wtf to use mock version of Global Config instead,
- // and set getWtfHandler() to return a mock wtf handler
- CLog.setGlobalConfigInstance(mMockGlobalConfig);
- EasyMock.expect(mMockGlobalConfig.getWtfHandler()).andReturn(mMockWtfHandler).anyTimes();
- EasyMock.replay(mMockGlobalConfig);
-
- // expect onTerribleFailure to be called once per CLog.wtf() call
- EasyMock.expect(mMockWtfHandler.onTerribleFailure(EasyMock.<String> anyObject(),
- EasyMock.<Throwable> anyObject())).andReturn(true).times(4);
- EasyMock.replay(mMockWtfHandler);
-
- CLog.wtf("this is CLog.wtf");
- CLog.wtf(new Throwable("this is CLog.wtf as a throwable"));
- CLog.wtf("this is CLog.wtf with a format string: %s has length %d",
- STRING, STRING.length());
- CLog.wtf("this is CLog.wtf with a throwable", new Throwable("this is my throwable"));
- EasyMock.verify(mMockWtfHandler);
- }
-
- /**
- * Verify scenario where no wtf handler has been configured when CLog.wtf() is called
- */
- public void testCLog_wtf_wtfHandlerNotSet() {
- // force CLog.wtf to use mock version of Global Config instead,
- // and set getWtfHandler() to return null (simulating no wtf handler being set)
- CLog.setGlobalConfigInstance(mMockGlobalConfig);
- EasyMock.expect(mMockGlobalConfig.getWtfHandler()).andReturn(null);
- EasyMock.replay(mMockGlobalConfig);
-
- // by not setting any expect on mMockWtfHandler,
- // EasyMock will verify that 0 calls are made to wtf handler
- EasyMock.replay(mMockWtfHandler);
-
- CLog.wtf("this is CLog.wtf without any handler set");
- EasyMock.verify(mMockWtfHandler);
- }
-
- /**
* Verify that getClassName can get the desired class name from the stack trace.
*/
public void testCLog_getClassName() {
diff --git a/tests/src/com/android/tradefed/log/SimpleFileLoggerTest.java b/tests/src/com/android/tradefed/log/SimpleFileLoggerTest.java
new file mode 100644
index 0000000..b6ae74c
--- /dev/null
+++ b/tests/src/com/android/tradefed/log/SimpleFileLoggerTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.log;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import static org.hamcrest.CoreMatchers.endsWith;
+import static org.junit.Assert.assertTrue;
+
+import com.android.ddmlib.Log.LogLevel;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Unit tests for {@link SimpleFileLogger}. */
+@RunWith(JUnit4.class)
+public class SimpleFileLoggerTest {
+
+ private static final String LOG_TAG = SimpleFileLoggerTest.class.getSimpleName();
+
+ private File mLogFile;
+ private SimpleFileLogger mLogger;
+
+ @Before
+ public void setUp() throws IOException, ConfigurationException {
+ mLogFile = Files.createTempFile(LOG_TAG, null).toFile();
+ mLogger = new SimpleFileLogger();
+ OptionSetter setter = new OptionSetter(mLogger);
+ setter.setOptionValue("path", mLogFile.getAbsolutePath());
+ }
+
+ @After
+ public void tearDown() {
+ if (mLogger != null) {
+ mLogger.closeLog();
+ }
+ FileUtil.deleteFile(mLogFile);
+ }
+
+ @Test
+ public void testPrintLog() throws IOException {
+ mLogger.init();
+ mLogger.printLog(LogLevel.DEBUG, LOG_TAG, "debug");
+ mLogger.printLog(LogLevel.INFO, LOG_TAG, "info");
+ mLogger.printLog(LogLevel.WARN, LOG_TAG, "warn");
+ mLogger.printLog(LogLevel.ERROR, LOG_TAG, "error");
+
+ List<String> lines = readLines(new FileInputStream(mLogFile));
+ assertEquals(4, lines.size());
+ assertThat(lines.get(0), endsWith(String.format("D/%s: %s", LOG_TAG, "debug")));
+ assertThat(lines.get(1), endsWith(String.format("I/%s: %s", LOG_TAG, "info")));
+ assertThat(lines.get(2), endsWith(String.format("W/%s: %s", LOG_TAG, "warn")));
+ assertThat(lines.get(3), endsWith(String.format("E/%s: %s", LOG_TAG, "error")));
+ }
+
+ @Test
+ public void testGetLog() throws IOException {
+ mLogger.init();
+ mLogger.printLog(LogLevel.INFO, LOG_TAG, "hello world");
+ // getting the log is equivalent to reading the file
+ List<String> expected = readLines(new FileInputStream(mLogFile));
+ List<String> actual = readLines(mLogger.getLog().createInputStream());
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testInit() throws IOException {
+ // logging is ignored if not initialized
+ mLogger.printLog(LogLevel.INFO, LOG_TAG, "hello world");
+ List<String> lines = readLines(new FileInputStream(mLogFile));
+ assertTrue(lines.isEmpty());
+ }
+
+ @Test
+ public void testCloseLog() throws IOException {
+ mLogger.init();
+ mLogger.closeLog();
+ // logging is ignored if closed
+ mLogger.printLog(LogLevel.INFO, LOG_TAG, "hello world");
+ List<String> lines = readLines(new FileInputStream(mLogFile));
+ assertTrue(lines.isEmpty());
+ }
+
+ @Test
+ public void testClone() throws IOException {
+ mLogger.init();
+ mLogger.printLog(LogLevel.DEBUG, LOG_TAG, "original");
+
+ // clone will append to same log file
+ SimpleFileLogger clone = mLogger.clone();
+ try {
+ clone.init();
+ clone.printLog(LogLevel.DEBUG, LOG_TAG, "clone");
+ } finally {
+ clone.closeLog();
+ }
+
+ List<String> lines = readLines(new FileInputStream(mLogFile));
+ assertEquals(2, lines.size());
+ assertThat(lines.get(0), endsWith("original"));
+ assertThat(lines.get(1), endsWith("clone"));
+ }
+
+ // Read input stream as a list of strings.
+ private static List<String> readLines(InputStream is) {
+ return new BufferedReader(new InputStreamReader(is)).lines().collect(Collectors.toList());
+ }
+}
diff --git a/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java b/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
index 384b04a..8b37194 100644
--- a/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
+++ b/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
@@ -45,6 +45,7 @@
private static final String STATS_KEY_VAR = "var";
private static final String STATS_KEY_STDEV = "stdev";
private static final String STATS_KEY_MEDIAN = "median";
+ private static final String STATS_KEY_TOTAL = "total";
// Separator for final upload
private static final String STATS_KEY_SEPARATOR = "-";
@@ -71,6 +72,7 @@
singularDoubleStats.put(STATS_KEY_VAR, "0.54");
singularDoubleStats.put(STATS_KEY_STDEV, "0.73");
singularDoubleStats.put(STATS_KEY_MEDIAN, "2.00");
+ singularDoubleStats.put(STATS_KEY_TOTAL, "6.00");
// Construct ListMultimap of multiple iterations of test metrics.
// Stores processed metrics which is overwitten with every test; this is consistent with
@@ -146,6 +148,16 @@
.build()
.getMeasurements()
.getSingleString());
+ Assert.assertTrue(
+ processedMetrics.containsKey(
+ String.join(STATS_KEY_SEPARATOR, singularDoubleKey, STATS_KEY_TOTAL)));
+ Assert.assertEquals(
+ singularDoubleStats.get(STATS_KEY_TOTAL),
+ processedMetrics
+ .get(String.join(STATS_KEY_SEPARATOR, singularDoubleKey, STATS_KEY_TOTAL))
+ .build()
+ .getMeasurements()
+ .getSingleString());
}
/** Test correct aggregation of list double metrics. */
@@ -162,6 +174,7 @@
listDoubleStats.put(STATS_KEY_VAR, "0.36");
listDoubleStats.put(STATS_KEY_STDEV, "0.60");
listDoubleStats.put(STATS_KEY_MEDIAN, "2.05");
+ listDoubleStats.put(STATS_KEY_TOTAL, "12.10");
// Stores processed metrics which is overwitten with every test; this is consistent with
// the current reporting behavior. We only test the correctness on the final metrics values.
@@ -233,6 +246,16 @@
.build()
.getMeasurements()
.getSingleString());
+ Assert.assertTrue(
+ processedMetrics.containsKey(
+ listDoubleKey + STATS_KEY_SEPARATOR + STATS_KEY_TOTAL));
+ Assert.assertEquals(
+ listDoubleStats.get(STATS_KEY_TOTAL),
+ processedMetrics
+ .get(listDoubleKey + STATS_KEY_SEPARATOR + STATS_KEY_TOTAL)
+ .build()
+ .getMeasurements()
+ .getSingleString());
}
@@ -274,6 +297,9 @@
Assert.assertFalse(
processedMetrics.containsKey(
String.join(STATS_KEY_SEPARATOR, nonNumericKey, STATS_KEY_MEDIAN)));
+ Assert.assertFalse(
+ processedMetrics.containsKey(
+ String.join(STATS_KEY_SEPARATOR, nonNumericKey, STATS_KEY_TOTAL)));
}
/** Test empty result. */
@@ -312,6 +338,9 @@
Assert.assertFalse(
processedMetrics.containsKey(
String.join(STATS_KEY_SEPARATOR, emptyResultKey, STATS_KEY_MEDIAN)));
+ Assert.assertFalse(
+ processedMetrics.containsKey(
+ String.join(STATS_KEY_SEPARATOR, emptyResultKey, STATS_KEY_TOTAL)));
}
/** Test single run. */
@@ -392,6 +421,16 @@
.build()
.getMeasurements()
.getSingleString());
+ Assert.assertTrue(
+ processedMetrics.containsKey(
+ String.join(STATS_KEY_SEPARATOR, singleRunKey, STATS_KEY_TOTAL)));
+ Assert.assertEquals(
+ singleRunVal,
+ processedMetrics
+ .get(String.join(STATS_KEY_SEPARATOR, singleRunKey, STATS_KEY_TOTAL))
+ .build()
+ .getMeasurements()
+ .getSingleString());
}
@@ -430,6 +469,9 @@
Assert.assertTrue(
processedMetrics.containsKey(
String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_MEDIAN)));
+ Assert.assertTrue(
+ processedMetrics.containsKey(
+ String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_TOTAL)));
}
/**
diff --git a/tests/src/com/android/tradefed/result/LegacySubprocessResultsReporterTest.java b/tests/src/com/android/tradefed/result/LegacySubprocessResultsReporterTest.java
index 5c4854f..0b18e11 100644
--- a/tests/src/com/android/tradefed/result/LegacySubprocessResultsReporterTest.java
+++ b/tests/src/com/android/tradefed/result/LegacySubprocessResultsReporterTest.java
@@ -105,7 +105,8 @@
Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
- mMockListener.testRunStarted("test run", 2);
+ mMockListener.testRunStarted(
+ EasyMock.eq("test run"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
mMockListener.testRunEnded(50, TfMetricProtoUtil.upgradeConvert(map));
EasyMock.replay(mMockListener);
mReporter.testRunStarted("test run", 2);
diff --git a/tests/src/com/android/tradefed/result/SubprocessResultsReporterTest.java b/tests/src/com/android/tradefed/result/SubprocessResultsReporterTest.java
index 5fe32ca..1c2ea0d 100644
--- a/tests/src/com/android/tradefed/result/SubprocessResultsReporterTest.java
+++ b/tests/src/com/android/tradefed/result/SubprocessResultsReporterTest.java
@@ -69,13 +69,15 @@
File tmpReportFile = FileUtil.createTempFile("subprocess-reporter", "unittest");
try {
setter.setOptionValue("subprocess-report-file", tmpReportFile.getAbsolutePath());
- mReporter.testRunStarted("TEST", 5);
+ mReporter.testRunStarted("TEST", 5, 1, 500);
mReporter.testRunEnded(100, new HashMap<String, Metric>());
String content = FileUtil.readStringFromFile(tmpReportFile);
- assertEquals(
- "TEST_RUN_STARTED {\"testCount\":5,\"runName\":\"TEST\",\"runAttempt\":0}\n"
- + "TEST_RUN_ENDED {\"time\":100}\n",
- content);
+ assertTrue(content.contains("TEST_RUN_STARTED"));
+ assertTrue(content.contains("\"testCount\":5"));
+ assertTrue(content.contains("\"runName\":\"TEST\""));
+ assertTrue(content.contains("\"start_time\":500"));
+ assertTrue(content.contains("\"runAttempt\":1"));
+ assertTrue(content.contains("TEST_RUN_ENDED {\"time\":100}\n"));
} finally {
FileUtil.deleteFile(tmpReportFile);
}
diff --git a/tests/src/com/android/tradefed/result/TestResultTest.java b/tests/src/com/android/tradefed/result/TestResultTest.java
index 404c071..cb971e2 100644
--- a/tests/src/com/android/tradefed/result/TestResultTest.java
+++ b/tests/src/com/android/tradefed/result/TestResultTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.testtype.retry.MergeStrategy;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/tests/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParserTest.java b/tests/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParserTest.java
new file mode 100644
index 0000000..f154fc5
--- /dev/null
+++ b/tests/src/com/android/tradefed/result/ddmlib/InstrumentationResultProtoParserTest.java
@@ -0,0 +1,1096 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.ddmlib;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.commands.am.InstrumentationData.ResultsBundle;
+import com.android.commands.am.InstrumentationData.ResultsBundleEntry;
+import com.android.commands.am.InstrumentationData.Session;
+import com.android.commands.am.InstrumentationData.SessionStatus;
+import com.android.commands.am.InstrumentationData.SessionStatusCode;
+import com.android.commands.am.InstrumentationData.TestStatus;
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.InstrumentationResultParser;
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/** Unit tests for {@link InstrumentationResultProtoParser}. */
+@RunWith(JUnit4.class)
+public class InstrumentationResultProtoParserTest {
+
+ private InstrumentationResultProtoParser mParser;
+ private ITestRunListener mMockListener;
+
+ private static final String RUN_KEY = "testing";
+ private static final String CLASS_NAME_1 = "class_1";
+ private static final String METHOD_NAME_1 = "method_1";
+ private static final String CLASS_NAME_2 = "class_2";
+ private static final String METHOD_NAME_2 = "method_2";
+ private static final String TEST_FAILURE_MESSAGE_1 = "java.lang.AssertionError: No App";
+ private static final String RUN_FAILURE_MESSAGE = "Unable to find instrumentation info:";
+ private static final String TEST_COMPLETED_STATUS_1 = "Expected 2 tests, received 0";
+ private static final String TEST_COMPLETED_STATUS_2 = "Expected 2 tests, received 1";
+ private static final String INCOMPLETE_TEST_ERR_MSG_PREFIX = "Test failed to run"
+ + " to completion";
+ private static final String INCOMPLETE_RUN_ERR_MSG_PREFIX = "Test run failed to complete";
+ private static final String FATAL_EXCEPTION_MSG = "Fatal exception when running tests";
+
+ private File protoTestFile = null;
+
+ @Before
+ public void setUp() {
+ List<ITestRunListener> runListeners = new ArrayList<>();
+ mMockListener = EasyMock.createStrictMock(ITestRunListener.class);
+ runListeners.add(mMockListener);
+ mParser = new InstrumentationResultProtoParser(RUN_KEY, runListeners);
+ }
+
+ // Sample one test success instrumentation proto file in a test run.
+
+ // result_code: 1
+ // results {
+ // entries {
+ // key: "class"
+ // value_string: "android.platform.test.scenario.clock.OpenAppMicrobenchmark"
+ // }
+ // entries {
+ // key: "current"
+ // value_int: 1
+ // }
+ // entries {
+ // key: "id"
+ // value_string: "AndroidJUnitRunner"
+ // }
+ // entries {
+ // key: "numtests"
+ // value_int: 1
+ // }
+ // entries {
+ // key: "stream"
+ // value_string: "\nandroid.platform.test.scenario.clock.OpenAppMicrobenchmark:"
+ // }
+ // entries {
+ // key: "test"
+ // value_string: "testOpen"
+ // }
+ // }
+ // result_code: 2
+ // results {
+ // entries {
+ // key: "cold_startup_com.google.android.deskclock"
+ // value_string: "626"
+ // }
+ // }
+ //
+ // results {
+ // entries {
+ // key: "class"
+ // value_string: "android.platform.test.scenario.clock.OpenAppMicrobenchmark"
+ // }
+ // entries {
+ // key: "current"
+ // value_int: 1
+ // }
+ // entries {
+ // key: "id"
+ // value_string: "AndroidJUnitRunner"
+ // }
+ // entries {
+ // key: "numtests"
+ // value_int: 1
+ // }
+ // entries {
+ // key: "stream"
+ // value_string: "."
+ // }
+ // entries {
+ // key: "test"
+ // value_string: "testOpen"
+ // }
+ // }
+ //
+ // result_code: -1
+ // results {
+ // entries {
+ // key: "stream"
+ // value_string: "\n\nTime: 27.013\n\nOK (1 test)\n\n"
+ // }
+ // entries {
+ // key: "total_cpu_usage"
+ // value_string: "39584"
+ // }
+ // }
+
+ /**
+ * Test for the null input instrumentation results proto file.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testNullProtoFile() throws IOException {
+ protoTestFile = null;
+ mMockListener.testRunStarted(RUN_KEY, 0);
+ mMockListener.testRunFailed(EasyMock
+ .eq(InstrumentationResultProtoParser.NO_TEST_RESULTS_FILE));
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+ }
+
+ /**
+ * Test for the empty input instrumentation results proto file.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testEmptyProtoFile() throws IOException {
+ protoTestFile = File.createTempFile("tmp", ".pb");
+ mMockListener.testRunStarted(RUN_KEY, 0);
+ mMockListener.testRunFailed(EasyMock
+ .eq(InstrumentationResultProtoParser.NO_TEST_RESULTS_MSG));
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+ }
+
+ /**
+ * Test for the invalid input instrumentation results proto file.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testInvalidResultsProtoFile() throws IOException {
+ protoTestFile = File.createTempFile("tmp", ".pb");
+ FileOutputStream fout = new FileOutputStream(protoTestFile);
+ fout.write(65);
+ fout.close();
+ mMockListener.testRunStarted(RUN_KEY, 0);
+ mMockListener.testRunFailed(EasyMock
+ .eq(InstrumentationResultProtoParser.INVALID_TEST_RESULTS_FILE));
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+ }
+
+ /**
+ * Test for the no test results in input instrumentation results proto file.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testNoTestResults() throws IOException {
+
+ protoTestFile = buildNoTestResultsProtoFile();
+
+ mMockListener.testRunStarted(RUN_KEY, 0);
+ mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+ }
+
+ /**
+ * Test for one test success results in input instrumentation results proto file.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testOneTestSuccessWithMetrics() throws IOException {
+ protoTestFile = buildSingleTestMetricSuccessProtoFile();
+
+ TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ Capture<Map<String, String>> captureTestMetrics = new Capture<Map<String, String>>();
+
+ mMockListener.testRunStarted(RUN_KEY, 1);
+ mMockListener.testStarted(td);
+ mMockListener.testEnded(EasyMock.eq(td), EasyMock.capture(captureTestMetrics));
+ mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+
+ // Verify the test metrics
+ assertEquals("626",
+ captureTestMetrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureTestMetrics.getValue()
+ .get("metric_key2"));
+ }
+
+ /**
+ * Test for one test success result with multiple listeners in instrumentation results proto
+ * file.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testOneTestSuccessWithMultipleListeners() throws IOException {
+
+ List<ITestRunListener> runListeners = new ArrayList<>();
+ ITestRunListener mMockListener1 = EasyMock.createStrictMock(ITestRunListener.class);
+ ITestRunListener mMockListener2 = EasyMock.createStrictMock(ITestRunListener.class);
+ runListeners.add(mMockListener1);
+ runListeners.add(mMockListener2);
+
+ mParser = new InstrumentationResultProtoParser(RUN_KEY, runListeners);
+
+ protoTestFile = buildSingleTestMetricSuccessProtoFile();
+
+ TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ Capture<Map<String, String>> captureListener1Metrics = new Capture<Map<String, String>>();
+ Capture<Map<String, String>> captureListener2Metrics = new Capture<Map<String, String>>();
+
+ mMockListener1.testRunStarted(RUN_KEY, 1);
+ mMockListener1.testStarted(td);
+ mMockListener1.testEnded(EasyMock.eq(td), EasyMock.capture(captureListener1Metrics));
+ mMockListener1.testRunEnded(27013, Collections.emptyMap());
+
+ mMockListener2.testRunStarted(RUN_KEY, 1);
+ mMockListener2.testStarted(td);
+ mMockListener2.testEnded(EasyMock.eq(td), EasyMock.capture(captureListener2Metrics));
+ mMockListener2.testRunEnded(27013, Collections.emptyMap());
+
+ EasyMock.replay(mMockListener1);
+ EasyMock.replay(mMockListener2);
+ mParser.processProtoFile(protoTestFile);
+ EasyMock.verify(mMockListener1);
+ EasyMock.verify(mMockListener2);
+
+ // Verify the test metrics
+ assertEquals("626",
+ captureListener1Metrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureListener1Metrics.getValue()
+ .get("metric_key2"));
+
+ // Verify the test metrics
+ assertEquals("626",
+ captureListener2Metrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureListener2Metrics.getValue()
+ .get("metric_key2"));
+ }
+
+ /**
+ * Test for test run with the metrics.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testOneRunSuccessWithMetrics() throws IOException {
+ protoTestFile = buildRunMetricSuccessProtoFile();
+
+ TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ Capture<Map<String, String>> captureRunMetrics = new Capture<Map<String, String>>();
+ mMockListener.testRunStarted(RUN_KEY, 1);
+ mMockListener.testStarted(td);
+ mMockListener.testEnded(td, Collections.emptyMap());
+ mMockListener.testRunEnded(EasyMock.eq(27013L), EasyMock.capture(captureRunMetrics));
+
+ processProtoAndVerify(protoTestFile);
+
+ // Verify run metrics
+ assertEquals("39584",
+ captureRunMetrics.getValue().get("run_metric_key"));
+ }
+
+ /**
+ * Test for test metrics and test run metrics in instrumentation proto file.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testOneTestAndRunSuccessWithMetrics() throws IOException {
+ protoTestFile = buildTestAndRunMetricSuccessProtoFile();
+
+ TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ Capture<Map<String, String>> captureTestMetrics = new Capture<Map<String, String>>();
+ Capture<Map<String, String>> captureRunMetrics = new Capture<Map<String, String>>();
+ mMockListener.testRunStarted(RUN_KEY, 1);
+ mMockListener.testStarted(td);
+ mMockListener.testEnded(EasyMock.eq(td), EasyMock.capture(captureTestMetrics));
+ mMockListener.testRunEnded(EasyMock.eq(27013L), EasyMock.capture(captureRunMetrics));
+
+ processProtoAndVerify(protoTestFile);
+
+ // Verify the test metrics
+ assertEquals("626",
+ captureTestMetrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureTestMetrics.getValue()
+ .get("metric_key2"));
+
+ // Verify run metrics
+ assertEquals("39584",
+ captureRunMetrics.getValue().get("run_metric_key"));
+ }
+
+ /**
+ * Test for multiple test success with metrics.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testMultipleTestSuccessWithMetrics() throws IOException {
+ protoTestFile = buildMultipleTestAndRunMetricSuccessProtoFile();
+
+ TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ TestIdentifier td2 = new TestIdentifier(CLASS_NAME_2, METHOD_NAME_2);
+
+ Capture<Map<String, String>> captureTest1Metrics = new Capture<Map<String, String>>();
+ Capture<Map<String, String>> captureTest2Metrics = new Capture<Map<String, String>>();
+ Capture<Map<String, String>> captureRunMetrics = new Capture<Map<String, String>>();
+
+ mMockListener.testRunStarted(RUN_KEY, 2);
+ mMockListener.testStarted(td1);
+ mMockListener.testEnded(EasyMock.eq(td1), EasyMock.capture(captureTest1Metrics));
+ mMockListener.testStarted(td2);
+ mMockListener.testEnded(EasyMock.eq(td2), EasyMock.capture(captureTest2Metrics));
+ mMockListener.testRunEnded(EasyMock.eq(27013L), EasyMock.capture(captureRunMetrics));
+
+ processProtoAndVerify(protoTestFile);
+
+ // Verify the test1 and test2 metrics
+ assertEquals("626",
+ captureTest1Metrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureTest1Metrics.getValue()
+ .get("metric_key2"));
+ assertEquals("626",
+ captureTest2Metrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureTest2Metrics.getValue()
+ .get("metric_key2"));
+
+ // Verify run metrics
+ assertEquals("39584",
+ captureRunMetrics.getValue().get("run_metric_key"));
+ }
+
+ /**
+ * Test for one test failure.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testOneTestFailure() throws IOException {
+ protoTestFile = buildSingleTestFailureProtoFile();
+
+ TestIdentifier td = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ Capture<Map<String, String>> captureTestMetrics = new Capture<Map<String, String>>();
+
+ mMockListener.testRunStarted(RUN_KEY, 1);
+ mMockListener.testStarted(td);
+ mMockListener.testFailed(EasyMock.eq(td), EasyMock.eq(TEST_FAILURE_MESSAGE_1));
+ mMockListener.testEnded(EasyMock.eq(td), EasyMock.capture(captureTestMetrics));
+ mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+
+ // Verify the test metrics
+ assertEquals("626",
+ captureTestMetrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureTestMetrics.getValue()
+ .get("metric_key2"));
+ }
+
+ /**
+ * Test for one test pass and one test failure.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testOneTestPassOneTestFailure() throws IOException {
+ protoTestFile = buildOneTestPassOneTestFailProtoFile();
+
+ TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ TestIdentifier td2 = new TestIdentifier(CLASS_NAME_2, METHOD_NAME_2);
+
+ Capture<Map<String, String>> captureTest1Metrics = new Capture<Map<String, String>>();
+
+ mMockListener.testRunStarted(RUN_KEY, 2);
+ mMockListener.testStarted(td1);
+ mMockListener.testEnded(EasyMock.eq(td1), EasyMock.capture(captureTest1Metrics));
+
+ mMockListener.testStarted(td2);
+ mMockListener.testFailed(EasyMock.eq(td2), EasyMock.eq(TEST_FAILURE_MESSAGE_1));
+ mMockListener.testEnded(td2, Collections.emptyMap());
+
+ mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+
+ // Verify the test metrics
+ assertEquals("626",
+ captureTest1Metrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureTest1Metrics.getValue()
+ .get("metric_key2"));
+ }
+
+ /**
+ * Test for all tests incomplete in a test run.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testAllTestsIncomplete() throws IOException {
+ protoTestFile = buildTestsIncompleteProtoFile();
+ Capture<String> testOutputErrorMessage = new Capture<>();
+ Capture<String> runOutputErrorMessage = new Capture<>();
+
+ TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ mMockListener.testRunStarted(RUN_KEY, 2);
+ mMockListener.testStarted(td1);
+ mMockListener.testFailed(EasyMock.eq(td1), EasyMock.capture(testOutputErrorMessage));
+ mMockListener.testEnded(td1, Collections.emptyMap());
+ mMockListener.testRunFailed(EasyMock.capture(runOutputErrorMessage));
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+
+ assertTrue(testOutputErrorMessage.toString().contains(
+ INCOMPLETE_TEST_ERR_MSG_PREFIX));
+ assertTrue(testOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_1));
+ assertTrue(runOutputErrorMessage.toString().contains(
+ INCOMPLETE_RUN_ERR_MSG_PREFIX));
+ assertTrue(runOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_1));
+ }
+
+ /**
+ * Test for one test complete and another test partial status.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testPartialTestsIncomplete() throws IOException {
+ protoTestFile = buildPartialTestsIncompleteProtoFile();
+
+ Capture<String> testOutputErrorMessage = new Capture<>();
+ Capture<String> runOutputErrorMessage = new Capture<>();
+ TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ TestIdentifier td2 = new TestIdentifier(CLASS_NAME_2, METHOD_NAME_2);
+ Capture<Map<String, String>> captureTest1Metrics = new Capture<Map<String, String>>();
+
+ mMockListener.testRunStarted(RUN_KEY, 2);
+ mMockListener.testStarted(td1);
+ mMockListener.testEnded(EasyMock.eq(td1), EasyMock.capture(captureTest1Metrics));
+ mMockListener.testStarted(td2);
+ mMockListener.testFailed(EasyMock.eq(td2), EasyMock.capture(testOutputErrorMessage));
+ mMockListener.testEnded(td2, Collections.emptyMap());
+ mMockListener.testRunFailed(EasyMock.capture(runOutputErrorMessage));
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+
+ assertEquals("626",
+ captureTest1Metrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureTest1Metrics.getValue()
+ .get("metric_key2"));
+ assertTrue(testOutputErrorMessage.toString().contains(
+ INCOMPLETE_TEST_ERR_MSG_PREFIX));
+ assertTrue(testOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_2));
+ assertTrue(runOutputErrorMessage.toString().contains(
+ INCOMPLETE_RUN_ERR_MSG_PREFIX));
+ assertTrue(runOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_2));
+ }
+
+ /**
+ * Test 1 test completed, 1 test not started from two expected tests in a test run.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testOneTestNotStarted() throws IOException {
+ protoTestFile = buildOneTestNotStarted();
+ Capture<String> runOutputErrorMessage = new Capture<>();
+ TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ Capture<Map<String, String>> captureTest1Metrics = new Capture<Map<String, String>>();
+
+ mMockListener.testRunStarted(RUN_KEY, 2);
+ mMockListener.testStarted(td1);
+ mMockListener.testEnded(EasyMock.eq(td1), EasyMock.capture(captureTest1Metrics));
+ mMockListener.testRunFailed(EasyMock.capture(runOutputErrorMessage));
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+
+ assertEquals("626",
+ captureTest1Metrics.getValue().get("metric_key1"));
+ assertEquals("1",
+ captureTest1Metrics.getValue()
+ .get("metric_key2"));
+ assertTrue(runOutputErrorMessage.toString().contains(
+ INCOMPLETE_RUN_ERR_MSG_PREFIX));
+ assertTrue(runOutputErrorMessage.toString().contains(TEST_COMPLETED_STATUS_2));
+ }
+
+ /**
+ * Test for time stamp missing when the time stamp parsing is enforced.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testTimeStampMissing() throws IOException {
+ // we enforce the time stamp
+ mParser.setEnforceTimeStamp(true);
+ protoTestFile = buildInvalidTimeStampResultsProto(false);
+
+ mMockListener.testRunStarted(RUN_KEY, 0);
+ mMockListener.testRunFailed(InstrumentationResultParser.INVALID_OUTPUT_ERR_MSG);
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+ }
+
+ /**
+ * Test for no time stamp parsing error when the time stamp parsing is not enforced.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testTimeStampMissingNotEnforced() throws IOException {
+ protoTestFile = buildInvalidTimeStampResultsProto(false);
+
+ mMockListener.testRunStarted(RUN_KEY, 0);
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+ }
+
+ /**
+ * Test for time stamp missing with the stack message when the time stamp parsing is enforced.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testMissingTimeStampWithStack() throws IOException {
+ mParser.setEnforceTimeStamp(true);
+ protoTestFile = buildInvalidTimeStampResultsProto(true);
+
+ Capture<String> capture = new Capture<>();
+ mMockListener.testRunStarted(RUN_KEY, 0);
+ mMockListener.testRunFailed(EasyMock.capture(capture));
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+
+ String failure = capture.getValue();
+ assertTrue(failure.startsWith(InstrumentationResultParser.INVALID_OUTPUT_ERR_MSG));
+ assertTrue(failure.contains(FATAL_EXCEPTION_MSG));
+ }
+
+ /**
+ * Tests parsing the fatal error output of an instrumentation invoked with "-e log true". Since
+ * it is log only, it will not report directly the failure, but the stream should still be
+ * populated.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testDirectFailure() throws IOException {
+
+ mParser.setEnforceTimeStamp(true);
+ protoTestFile = buildValidTimeStampWithFatalExceptionResultsProto();
+
+ Capture<String> capture = new Capture<>();
+ mMockListener.testRunStarted(RUN_KEY, 0);
+ mMockListener.testRunFailed(EasyMock.capture(capture));
+ mMockListener.testRunEnded(0, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+
+ String failure = capture.getValue();
+ assertTrue(failure.contains("java.lang.RuntimeException: it failed super fast."));
+ }
+
+ /**
+ * Tests for ignore test status from the proto output.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testIgnoreProtoResult() throws IOException {
+
+ mParser.setEnforceTimeStamp(true);
+ protoTestFile = buildTestIgnoredResultsProto();
+
+ mMockListener.testRunStarted(RUN_KEY, 1);
+ TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ mMockListener.testStarted(td1);
+ mMockListener.testIgnored(td1);
+ mMockListener.testEnded(td1, Collections.emptyMap());
+ mMockListener.testRunEnded(27013, Collections.emptyMap());
+
+ processProtoAndVerify(protoTestFile);
+ }
+
+ /**
+ * Tests for assumption failure test status from the proto output.
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testAssumptionProtoResult() throws IOException {
+ mParser.setEnforceTimeStamp(true);
+ protoTestFile = buildTestAssumptionResultsProto();
+
+ mMockListener.testRunStarted(RUN_KEY, 1);
+ TestIdentifier td1 = new TestIdentifier(CLASS_NAME_1, METHOD_NAME_1);
+ mMockListener.testStarted(td1);
+ mMockListener.testAssumptionFailure(EasyMock.eq(td1),
+ EasyMock.startsWith(
+ "org.junit.AssumptionViolatedException:"
+ + " got: <false>, expected: is <true>"));
+ mMockListener.testEnded(td1, Collections.emptyMap());
+ mMockListener.testRunEnded(27013, Collections.emptyMap());
+ processProtoAndVerify(protoTestFile);
+
+ }
+
+ @After
+ public void tearDown() {
+ if (protoTestFile != null && protoTestFile.exists()) {
+ protoTestFile.delete();
+ }
+ }
+
+ private void processProtoAndVerify(File protoTestFile) throws IOException {
+ EasyMock.replay(mMockListener);
+ mParser.processProtoFile(protoTestFile);
+ EasyMock.verify(mMockListener);
+ }
+
+ private File buildNoTestResultsProtoFile() throws IOException {
+ Session sessionProto = Session.newBuilder()
+ .setSessionStatus(getSessionStatusProto(false, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildSingleTestMetricSuccessProtoFile() throws IOException {
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+ // Test Metric
+ testStatusList.add(getTestStatusProto(true));
+ // Test End
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, false, false));
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .setSessionStatus(getSessionStatusProto(false, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildRunMetricSuccessProtoFile() throws IOException {
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(false));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, false, false));
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .setSessionStatus(getSessionStatusProto(true, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildTestAndRunMetricSuccessProtoFile() throws IOException {
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(true));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, false, false));
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .setSessionStatus(getSessionStatusProto(true, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildMultipleTestAndRunMetricSuccessProtoFile() throws IOException {
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(true));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, false, false));
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(true));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, false, false));
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .setSessionStatus(getSessionStatusProto(true, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildSingleTestFailureProtoFile() throws IOException {
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(true));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, false, true));
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .setSessionStatus(getSessionStatusProto(false, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildOneTestPassOneTestFailProtoFile() throws IOException {
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(true));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, false, false));
+ testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(false));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, false, true));
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .setSessionStatus(getSessionStatusProto(false, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildTestsIncompleteProtoFile() throws IOException {
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+
+ }
+
+ private File buildPartialTestsIncompleteProtoFile() throws IOException {
+
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(true));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, false, false));
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_2, METHOD_NAME_2, 2, 2, true, false));
+
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildOneTestNotStarted() throws IOException {
+
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, true, false));
+ // Test status without metrics.
+ testStatusList.add(getTestStatusProto(true));
+ // Test End.
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 2, false, false));
+
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildInvalidTimeStampResultsProto(boolean isWithStack) throws IOException {
+
+ List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+
+ if (isWithStack) {
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("stream")
+ .setValueString(FATAL_EXCEPTION_MSG
+ + " java.lang.IllegalArgumentException: Ambiguous arguments: "
+ + "cannot provide both test package and test class(es) to run")
+ .build());
+ } else {
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("stream").setValueString("")
+ .build());
+ }
+
+ SessionStatus sessionStatus = SessionStatus.newBuilder().setResultCode(-1)
+ .setStatusCode(SessionStatusCode.SESSION_FINISHED)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build();
+
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder()
+ .setSessionStatus(sessionStatus).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildValidTimeStampWithFatalExceptionResultsProto() throws IOException {
+ List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("stream")
+ .setValueString(FATAL_EXCEPTION_MSG
+ + "Time: 0 \n"
+ + "1) Fatal exception when running tests"
+ + "java.lang.RuntimeException: it failed super fast."
+ + "at stackstack"
+ )
+ .build());
+
+ SessionStatus sessionStatus = SessionStatus.newBuilder().setResultCode(-1)
+ .setStatusCode(SessionStatusCode.SESSION_FINISHED)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build();
+
+ // Session with metrics.
+ Session sessionProto = Session.newBuilder()
+ .setSessionStatus(sessionStatus).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildTestIgnoredResultsProto() throws IOException {
+
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+ // Test start
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+
+ // Test ignore status result.
+ List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("class").setValueString(CLASS_NAME_1)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("current").setValueInt(1)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("id")
+ .setValueString("AndroidJUnitRunner").build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("numtests").setValueInt(1)
+ .build());
+
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("test").setValueString(METHOD_NAME_1)
+ .build());
+
+ testStatusList.add(TestStatus.newBuilder().setResultCode(-3)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build());
+
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .setSessionStatus(getSessionStatusProto(false, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ private File buildTestAssumptionResultsProto() throws IOException {
+
+ List<TestStatus> testStatusList = new LinkedList<TestStatus>();
+
+ // Test start
+ testStatusList.add(getTestInfoProto(CLASS_NAME_1, METHOD_NAME_1, 1, 1, true, false));
+
+ // Test ignore status result.
+ List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("class").setValueString(CLASS_NAME_1)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("current").setValueInt(1)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("id")
+ .setValueString("AndroidJUnitRunner").build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("numtests").setValueInt(1)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("test").setValueString(METHOD_NAME_1)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("stack").setValueString(
+ "org.junit.AssumptionViolatedException: got: <false>, expected: is <true>")
+ .build());
+ testStatusList.add(TestStatus.newBuilder().setResultCode(-4)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build());
+
+ Session sessionProto = Session.newBuilder().addAllTestStatus(testStatusList)
+ .setSessionStatus(getSessionStatusProto(false, false)).build();
+ File protoFile = File.createTempFile("tmp", ".pb");
+ sessionProto.writeTo(new FileOutputStream(protoFile));
+ return protoFile;
+ }
+
+ /**
+ * Add test status proto message based on the args supplied to this method.
+ *
+ * @param className class name where the test method is.
+ * @param methodName method name currently running.
+ * @param current current number of the test.
+ * @param numTests total number of test.
+ * @param isStart true is if it is start of the test otherwise treated as end of the test.
+ * @param isFailure true if the test if failed.
+ * @return
+ */
+ private TestStatus getTestInfoProto(String className, String methodName, int current,
+ int numTests, boolean isStart, boolean isFailure) {
+ List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("class").setValueString(className)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("current").setValueInt(current)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("id")
+ .setValueString("AndroidJUnitRunner").build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("numtests").setValueInt(numTests)
+ .build());
+
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("test").setValueString(methodName)
+ .build());
+
+ if (isFailure) {
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("stack")
+ .setValueString(TEST_FAILURE_MESSAGE_1)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("stream")
+ .setValueString(TEST_FAILURE_MESSAGE_1)
+ .build());
+ // Test failure will have result code "-2"
+ return TestStatus.newBuilder().setResultCode(-2)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build();
+ }
+
+ entryList.add(ResultsBundleEntry.newBuilder().setKey("stream").setValueString("\nabc:")
+ .build());
+
+ if (isStart) {
+ // Test start will have result code 1.
+ return TestStatus.newBuilder().setResultCode(1)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build();
+ }
+
+ return TestStatus.newBuilder()
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build()).build();
+ }
+
+ /**
+ * Add test status with the metrics in the proto result file.
+ *
+ * @param isWithMetrics if false metric will be ignored.
+ * @return
+ */
+ private TestStatus getTestStatusProto(boolean isWithMetrics) {
+ List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+ if (isWithMetrics) {
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("metric_key1").setValueString("626")
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("metric_key2").setValueString("1")
+ .build());
+ }
+
+ // Metric status will be in progress
+ return TestStatus.newBuilder().setResultCode(2)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build();
+ }
+
+ /**
+ * Add session status message in the proto result file based on the args supplied to this
+ * method.
+ *
+ * @param isWithMetrics is true then add metrics to the session message.
+ * @param isFailure is true then failure message will be added to the final message.
+ * @return
+ */
+ private SessionStatus getSessionStatusProto(boolean isWithMetrics, boolean isFailure) {
+ List<ResultsBundleEntry> entryList = new LinkedList<ResultsBundleEntry>();
+
+ if (isFailure) {
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("Error").setValueString(RUN_FAILURE_MESSAGE)
+ .build());
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("id").setValueString("ActivityManagerService")
+ .build());
+ return SessionStatus.newBuilder().setResultCode(-1)
+ .setStatusCode(SessionStatusCode.SESSION_FINISHED)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build();
+
+ }
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("stream").setValueString("\n\nTime: 27.013\n\nOK (1 test)\n\n")
+ .build());
+
+ if (isWithMetrics) {
+ entryList.add(ResultsBundleEntry.newBuilder()
+ .setKey("run_metric_key").setValueString("39584")
+ .build());
+ }
+
+
+
+ return SessionStatus.newBuilder().setResultCode(-1)
+ .setStatusCode(SessionStatusCode.SESSION_FINISHED)
+ .setResults(ResultsBundle.newBuilder().addAllEntries(entryList).build())
+ .build();
+ }
+}
diff --git a/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java b/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java
index db48f97..31e71d2 100644
--- a/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.fail;
import com.android.tradefed.config.ConfigurationDescriptor;
+import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.proto.InvocationContext.Context;
@@ -93,9 +94,10 @@
@Test
public void testWriteResults_periodic() throws Exception {
+ OptionSetter setter = new OptionSetter(mReporter);
+ setter.setOptionValue("periodic-proto-writing", "true");
+ setter.setOptionValue("proto-output-file", mOutput.getAbsolutePath());
TestDescription test1 = new TestDescription("class1", "test1");
- mReporter.setPeriodicWriting(true);
- mReporter.setFileOutput(mOutput);
IInvocationContext context = new InvocationContext();
context.setConfigurationDescriptor(new ConfigurationDescriptor());
context.addInvocationAttribute("test", "test");
diff --git a/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java b/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
index c82a4a9..1bcbe79 100644
--- a/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
+++ b/tests/src/com/android/tradefed/result/proto/ProtoResultReporterTest.java
@@ -91,12 +91,14 @@
mReporter.testModuleStarted(createModuleContext("arm32 module1"));
mReporter.testModuleEnded();
// Invocation ends
+ mReporter.invocationFailed(new NullPointerException());
mReporter.invocationEnded(500L);
// ------ Verify that everything was populated ------
assertNotNull(mFinalRecord.getTestRecordId());
assertNotNull(mFinalRecord.getStartTime().getSeconds());
assertNotNull(mFinalRecord.getEndTime().getSeconds());
+ assertNotNull(mFinalRecord.getDebugInfo());
// The invocation has 2 modules
assertEquals(2, mFinalRecord.getChildrenCount());
diff --git a/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
index c142c05..dee9e5d 100644
--- a/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
+++ b/tests/src/com/android/tradefed/suite/checker/SystemServerStatusCheckerTest.java
@@ -16,11 +16,11 @@
package com.android.tradefed.suite.checker;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
+import com.android.tradefed.util.ProcessInfo;
import org.easymock.EasyMock;
import org.junit.Before;
@@ -28,6 +28,9 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
+import java.util.HashMap;
+import java.util.Map;
+
/** Unit tests for {@link SystemServerStatusChecker} */
@RunWith(JUnit4.class)
public class SystemServerStatusCheckerTest {
@@ -38,6 +41,7 @@
@Before
public void setUp() {
mMockDevice = EasyMock.createMock(ITestDevice.class);
+ EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("SERIAL");
mChecker =
new SystemServerStatusChecker() {
@Override
@@ -47,11 +51,11 @@
};
}
- /** Test that system checker pass if the pid of system checker does not change. */
+ /** Test that system checker pass if system_server didn't restart. */
@Test
- public void testPidRemainUnchanged() throws Exception {
- EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
- .andReturn("914")
+ public void testSystemServerProcessNotRestarted() throws Exception {
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+ .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L))
.times(2);
EasyMock.replay(mMockDevice);
assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
@@ -59,13 +63,48 @@
EasyMock.verify(mMockDevice);
}
- /** Test that system checker fail if the pid of system checker does change. */
+ /** Test that system checker fail if system_server crashed and didn't come back. */
@Test
- public void testPidChanged() throws Exception {
- EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
- .andReturn("914\n");
- EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
- .andReturn("1024\n");
+ public void testSystemServerProcessCrashed() throws Exception {
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+ .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server"))).andReturn(null);
+ EasyMock.replay(mMockDevice);
+ assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
+ StatusCheckerResult result = mChecker.postExecutionCheck(mMockDevice);
+ assertEquals(CheckStatus.FAILED, result.getStatus());
+ assertTrue(result.isBugreportNeeded());
+ EasyMock.verify(mMockDevice);
+ }
+
+ /** Test that system checker fail if system_server restarted without device reboot. */
+ @Test
+ public void testSystemServerProcessRestartedWithoutDeviceReboot() throws Exception {
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+ .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+ .andReturn(new ProcessInfo("system", 1024, "system_server", 1559096000L));
+ EasyMock.expect(mMockDevice.getBootHistorySince(EasyMock.eq(1559091922L)))
+ .andReturn(new HashMap<Long, String>());
+ EasyMock.replay(mMockDevice);
+ assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
+ StatusCheckerResult result = mChecker.postExecutionCheck(mMockDevice);
+ assertEquals(CheckStatus.FAILED, result.getStatus());
+ assertTrue(result.isBugreportNeeded());
+ EasyMock.verify(mMockDevice);
+ }
+
+ /** Test that system checker fail if system_server restarted with device reboot. */
+ @Test
+ public void testSystemServerProcessRestartedWithUnintentionalDeviceReboot() throws Exception {
+ Map<Long, String> history = new HashMap<Long, String>();
+ history.put(1559095000L, "kernel_panic");
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+ .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+ .andReturn(new ProcessInfo("system", 1024, "system_server", 1559096000L));
+ EasyMock.expect(mMockDevice.getBootHistorySince(EasyMock.eq(1559091922L)))
+ .andReturn(history);
EasyMock.expect(mMockDevice.getLastExpectedRebootTimeMillis()).andReturn(200L);
EasyMock.replay(mMockDevice);
assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
@@ -76,47 +115,38 @@
}
/**
- * Test that if the pid changed but there was a Tradefed reboot, we still fail the checker just
- * in case, but don't collect a bugreport.
+ * Test that if the pid changed but there was a Tradefed reboot, we still not fail the checker.
*/
@Test
- public void testPidChanged_tfReboot() throws Exception {
- EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
- .andReturn("914\n");
- EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
- .andReturn("1024\n");
- // TF reboot was done
+ public void testSystemServerProcessRestartedWithIntentionalDeviceReboot() throws Exception {
+ Map<Long, String> history = new HashMap<Long, String>();
+ history.put(1559095000L, "reboot");
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+ .andReturn(new ProcessInfo("system", 914, "system_server", 1559091922L));
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server")))
+ .andReturn(new ProcessInfo("system", 1024, "system_server", 1559096000L));
+ EasyMock.expect(mMockDevice.getBootHistorySince(EasyMock.eq(1559091922L)))
+ .andReturn(history);
+ // TF reboot was triggered by host
EasyMock.expect(mMockDevice.getLastExpectedRebootTimeMillis()).andReturn(600L);
EasyMock.replay(mMockDevice);
assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
StatusCheckerResult result = mChecker.postExecutionCheck(mMockDevice);
- assertEquals(CheckStatus.FAILED, result.getStatus());
- assertFalse(result.isBugreportNeeded());
- EasyMock.verify(mMockDevice);
- }
-
- /** Test that if the format of the pid is unexpected, we skip the system checker. */
- @Test
- public void testFailToGetPid() throws Exception {
- EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
- .andReturn("not found\n");
- EasyMock.replay(mMockDevice);
- assertEquals(CheckStatus.SUCCESS, mChecker.preExecutionCheck(mMockDevice).getStatus());
- assertEquals(CheckStatus.SUCCESS, mChecker.postExecutionCheck(mMockDevice).getStatus());
+ assertEquals(CheckStatus.SUCCESS, result.getStatus());
EasyMock.verify(mMockDevice);
}
/**
- * Test that if the pid output is null, we fail the current preExecution but skip post
- * execution.
+ * Test that if fail to get system_server process at preExecutionCheck, we skip the
+ * system_server check in postExecution.
*/
@Test
- public void testPid_null() throws Exception {
- EasyMock.expect(mMockDevice.executeShellCommand(EasyMock.eq("pidof system_server")))
- .andReturn(null);
+ public void testFailToGetSystemServerProcess() throws Exception {
+ EasyMock.expect(mMockDevice.getProcessByName(EasyMock.eq("system_server"))).andReturn(null);
EasyMock.replay(mMockDevice);
assertEquals(CheckStatus.FAILED, mChecker.preExecutionCheck(mMockDevice).getStatus());
assertEquals(CheckStatus.SUCCESS, mChecker.postExecutionCheck(mMockDevice).getStatus());
EasyMock.verify(mMockDevice);
}
+
}
diff --git a/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
index b013373..c6f0ca5 100644
--- a/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
+++ b/tests/src/com/android/tradefed/suite/checker/UserCheckerTest.java
@@ -15,17 +15,16 @@
*/
package com.android.tradefed.suite.checker;
-import com.android.tradefed.suite.checker.UserChecker.DeviceUserState;
-
import java.util.Arrays;
-import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Map;
+import java.util.HashMap;
-import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
import org.junit.Test;
@@ -35,7 +34,12 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+
/** Unit tests for {@link UserChecker} */
@RunWith(JUnit4.class)
@@ -45,191 +49,306 @@
UserChecker checker = new UserChecker();
ITestDevice preDevice =
- mockDeviceUserState(
- /* userIds= */ new Integer[] {0},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
+ mockDeviceUserState(/* currentUser= */ 0, /* userIds= */ new Integer[] {0});
assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
ITestDevice postDevice =
- mockDeviceUserState(
- /* userIds= */ new Integer[] {0},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
+ mockDeviceUserState(/* currentUser= */ 0, /* userIds= */ new Integer[] {0});
assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
}
@Test
+ public void testSwitchIsSuccess() throws Exception {
+ UserChecker checker = new UserChecker();
+ OptionSetter mOptionSetter = new OptionSetter(checker);
+ mOptionSetter.setOptionValue("user-type", "system");
+ ITestDevice preDevice =
+ mockDeviceUserState(
+ /* currentUser= */ 10,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, 0},
+ /* isRunning= */ new Boolean[] {true, true});
+ when(preDevice.switchUser(0)).thenReturn(true);
+
+ assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+ verify(preDevice, never()).createUser(any(), anyBoolean(), anyBoolean());
+ verify(preDevice, times(1)).switchUser(0);
+
+ ITestDevice postDevice =
+ mockDeviceUserState(
+ /* currentUser= */ 0,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, 0},
+ /* isRunning= */ new Boolean[] {true, false});
+ assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
+ verify(postDevice, never()).createUser(any(), anyBoolean(), anyBoolean());
+ verify(postDevice, never()).switchUser(anyInt());
+ }
+
+ @Test
+ public void testCreateIsSuccess() throws Exception {
+ UserChecker checker = new UserChecker();
+ OptionSetter mOptionSetter = new OptionSetter(checker);
+ mOptionSetter.setOptionValue("user-type", "secondary");
+ ITestDevice preDevice =
+ mockDeviceUserState(
+ /* currentUser= */ 0,
+ /* userIds= */ new Integer[] {0},
+ /* flags= */ new Integer[] {0},
+ /* isRunning= */ new Boolean[] {true});
+ when(preDevice.createUser("Tfsecondary", false, false)).thenReturn(10);
+ when(preDevice.switchUser(10)).thenReturn(true);
+
+ assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+ verify(preDevice, times(1)).createUser("Tfsecondary", false, false);
+ verify(preDevice, times(1)).switchUser(10);
+
+ ITestDevice postDevice =
+ mockDeviceUserState(
+ /* currentUser= */ 10,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, 0},
+ /* isRunning= */ new Boolean[] {true, true});
+ assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
+ verify(postDevice, never()).removeUser(anyInt());
+ verify(postDevice, never()).switchUser(anyInt());
+ }
+
+ @Test
+ public void testCreateCleanup() throws Exception {
+ UserChecker checker = new UserChecker();
+ OptionSetter mOptionSetter = new OptionSetter(checker);
+ mOptionSetter.setOptionValue("user-type", "secondary");
+ mOptionSetter.setOptionValue("user-cleanup", "true");
+ ITestDevice preDevice =
+ mockDeviceUserState(
+ /* currentUser= */ 0,
+ /* userIds= */ new Integer[] {0},
+ /* flags= */ new Integer[] {0},
+ /* isRunning= */ new Boolean[] {true});
+ when(preDevice.createUser("Tfsecondary", false, false)).thenReturn(10);
+ when(preDevice.switchUser(10)).thenReturn(true);
+
+ assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+ verify(preDevice, times(1)).createUser("Tfsecondary", false, false);
+ verify(preDevice, times(1)).switchUser(10);
+
+ ITestDevice postDevice =
+ mockDeviceUserState(
+ /* currentUser= */ 10,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, 0},
+ /* isRunning= */ new Boolean[] {true, true});
+ when(postDevice.switchUser(0)).thenReturn(true);
+ when(postDevice.removeUser(10)).thenReturn(true);
+ assertEquals(CheckStatus.SUCCESS, checker.postExecutionCheck(postDevice).getStatus());
+ verify(postDevice, times(1)).switchUser(0);
+ verify(postDevice, times(1)).removeUser(10);
+ }
+
+ @Test
+ public void testCreateCleanup_cleanupFail() throws Exception {
+ UserChecker checker = new UserChecker();
+ OptionSetter mOptionSetter = new OptionSetter(checker);
+ mOptionSetter.setOptionValue("user-type", "secondary");
+ mOptionSetter.setOptionValue("user-cleanup", "true");
+ ITestDevice preDevice =
+ mockDeviceUserState(
+ /* currentUser= */ 0,
+ /* userIds= */ new Integer[] {0},
+ /* flags= */ new Integer[] {0},
+ /* isRunning= */ new Boolean[] {true});
+ when(preDevice.createUser("Tfsecondary", false, false)).thenReturn(10);
+ when(preDevice.switchUser(10)).thenReturn(true);
+
+ assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
+ verify(preDevice, times(1)).createUser("Tfsecondary", false, false);
+ verify(preDevice, times(1)).switchUser(10);
+
+ ITestDevice postDevice =
+ mockDeviceUserState(
+ /* currentUser= */ 10,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, 0},
+ /* isRunning= */ new Boolean[] {true, true});
+ when(postDevice.switchUser(0)).thenReturn(false);
+ when(postDevice.removeUser(10)).thenReturn(false);
+ StatusCheckerResult result = checker.postExecutionCheck(postDevice);
+ verify(postDevice, times(1)).switchUser(0);
+ verify(postDevice, times(1)).removeUser(10);
+ assertEquals(CheckStatus.FAILED, result.getStatus());
+ assertTrue(
+ result.getErrorMessage()
+ .contains("Failed to switch back to previous current user 0"));
+ assertTrue(result.getErrorMessage().contains("Failed to remove new user 10"));
+ }
+
+ @Test
/** Returns FAILED in the precessense of errors */
public void testAllErrorsIsFailed() throws Exception {
UserChecker checker = new UserChecker();
ITestDevice preDevice =
mockDeviceUserState(
+ /* currentUser= */ 10,
/* userIds= */ new Integer[] {0, 10, 11},
- /* runningUsers= */ new Integer[] {0, 10},
- /* currentUser= */ 10);
+ /* flags= */ new Integer[] {0, 0, 0},
+ /* isRunning= */ new Boolean[] {true, true, false});
assertEquals(CheckStatus.SUCCESS, checker.preExecutionCheck(preDevice).getStatus());
// User12 created, User11 deleted, User10 stopped, currentUser changed
ITestDevice postDevice =
mockDeviceUserState(
- /* userIds= */ new Integer[] {0, 10, 12},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
+ /* currentUser= */ 0,
+ /* userIds= */ new Integer[] {0, 10, 12},
+ /* flags= */ new Integer[] {0, 0, 0},
+ /* isRunning= */ new Boolean[] {true, false, false});
assertEquals(CheckStatus.FAILED, checker.postExecutionCheck(postDevice).getStatus());
}
@Test
- public void testSwitchToExistingOrCreateUserType() throws Exception {
+ public void testSwitchToSystem() throws Exception {
+ UserChecker checker = new UserChecker();
+ OptionSetter mOptionSetter = new OptionSetter(checker);
+ mOptionSetter.setOptionValue("user-type", "system");
+ ITestDevice device =
+ mockDeviceUserState(/* currentUser= */ 10, /* userIds= */ new Integer[] {0, 10});
+
+ when(device.switchUser(0)).thenReturn(true);
+
+ StatusCheckerResult result = checker.preExecutionCheck(device);
+ assertEquals(CheckStatus.SUCCESS, result.getStatus());
+ verify(device, never()).createUser(any(), anyBoolean(), anyBoolean());
+ verify(device, times(1)).switchUser(0);
+ }
+
+ @Test
+ public void testSwitchToSecondary() throws Exception {
UserChecker checker = new UserChecker();
OptionSetter mOptionSetter = new OptionSetter(checker);
mOptionSetter.setOptionValue("user-type", "secondary");
ITestDevice device =
- mockDeviceUserState(
- /* userIds= */ new Integer[] {0},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
+ mockDeviceUserState(/* currentUser= */ 0, /* userIds= */ new Integer[] {0, 10});
- when(device.getCurrentUser()).thenReturn(0);
- mockListUsers(device, new Integer[] {0});
- when(device.createUserNoThrow(UserChecker.DEFAULT_NAME)).thenReturn(10);
- when(device.getCurrentUser()).thenReturn(0);
- mockListUsers(device, new Integer[] {0, 10});
- when(device.isUserSecondary(10)).thenReturn(true);
when(device.switchUser(10)).thenReturn(true);
StatusCheckerResult result = checker.preExecutionCheck(device);
assertEquals(CheckStatus.SUCCESS, result.getStatus());
+ verify(device, never()).createUser(any(), anyBoolean(), anyBoolean());
verify(device, times(1)).switchUser(10);
}
@Test
- public void testSwitchToSecondaryUserCreateNewFail() throws Exception {
+ public void testSwitchToSecondary_fail() throws Exception {
UserChecker checker = new UserChecker();
OptionSetter mOptionSetter = new OptionSetter(checker);
mOptionSetter.setOptionValue("user-type", "secondary");
ITestDevice device =
- mockDeviceUserState(
- /* userIds= */ new Integer[] {0},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
+ mockDeviceUserState(/* currentUser= */ 0, /* userIds= */ new Integer[] {0, 10});
- when(device.getCurrentUser()).thenReturn(0);
- mockListUsers(device, new Integer[] {0});
- when(device.createUserNoThrow(UserChecker.DEFAULT_NAME)).thenReturn(-1);
+ when(device.switchUser(10)).thenReturn(false);
StatusCheckerResult result = checker.preExecutionCheck(device);
assertEquals(CheckStatus.FAILED, result.getStatus());
- verify(device, times(1)).createUserNoThrow(UserChecker.DEFAULT_NAME);
+ verify(device, never()).createUser(any(), anyBoolean(), anyBoolean());
+ verify(device, times(1)).switchUser(10);
}
@Test
- public void testFindRemovedUsers() throws Exception {
- DeviceUserState preState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0, 10},
- /* runningUsers= */ new Integer[] {0, 10},
- /* currentUser= */ 0);
- DeviceUserState postState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
+ public void testSwitchToGuest() throws Exception {
+ UserChecker checker = new UserChecker();
+ OptionSetter mOptionSetter = new OptionSetter(checker);
+ mOptionSetter.setOptionValue("user-type", "guest");
+ ITestDevice device =
+ mockDeviceUserState(
+ /* currentUser= */ 0,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, UserInfo.FLAG_GUEST},
+ /* isRunning= */ new Boolean[] {true, false});
- assertArrayEquals(new Integer[] {10}, preState.findRemovedUsers(postState).toArray());
+ when(device.switchUser(10)).thenReturn(true);
+
+ StatusCheckerResult result = checker.preExecutionCheck(device);
+ assertEquals(CheckStatus.SUCCESS, result.getStatus());
+ verify(device, never()).createUser(any(), anyBoolean(), anyBoolean());
+ verify(device, times(1)).switchUser(10);
}
@Test
- public void testFindAddedUsers() throws Exception {
- DeviceUserState preState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
- DeviceUserState postState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0, 10},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
+ public void testCreateSecondary() throws Exception {
+ UserChecker checker = new UserChecker();
+ OptionSetter mOptionSetter = new OptionSetter(checker);
+ mOptionSetter.setOptionValue("user-type", "secondary");
+ ITestDevice device =
+ mockDeviceUserState(/* currentUser= */ 0, /* userIds= */ new Integer[] {0});
- assertArrayEquals(new Integer[] {10}, preState.findAddedUsers(postState).toArray());
+ when(device.createUser("Tfsecondary", false, false)).thenReturn(10);
+ when(device.switchUser(10)).thenReturn(true);
+
+ StatusCheckerResult result = checker.preExecutionCheck(device);
+ assertEquals(CheckStatus.SUCCESS, result.getStatus());
+ verify(device, times(1)).createUser("Tfsecondary", false, false);
+ verify(device, times(1)).switchUser(10);
}
@Test
- public void testCurrentUserChanged() throws Exception {
- DeviceUserState preState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0, 10},
- /* runningUsers= */ new Integer[] {0, 10},
- /* currentUser= */ 10);
- DeviceUserState postState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0, 10},
- /* runningUsers= */ new Integer[] {0, 10},
- /* currentUser= */ 0);
+ public void testCreateGuest() throws Exception {
+ UserChecker checker = new UserChecker();
+ OptionSetter mOptionSetter = new OptionSetter(checker);
+ mOptionSetter.setOptionValue("user-type", "guest");
+ ITestDevice device =
+ mockDeviceUserState(/* currentUser= */ 0, /* userIds= */ new Integer[] {0});
- assertEquals(true, preState.currentUserChanged(postState));
+ when(device.createUser("Tfguest", /* guest= */ true, /* ephemeral= */ false))
+ .thenReturn(10);
+ when(device.switchUser(10)).thenReturn(true);
+
+ StatusCheckerResult result = checker.preExecutionCheck(device);
+ assertEquals(CheckStatus.SUCCESS, result.getStatus());
+ verify(device, times(1)).createUser("Tfguest", /* guest= */ true, /* ephemeral= */ false);
+ verify(device, times(1)).switchUser(10);
}
- @Test
- public void testfindStartedUsers() throws Exception {
- DeviceUserState preState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0, 10},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
- DeviceUserState postState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0, 10},
- /* runningUsers= */ new Integer[] {0, 10},
- /* currentUser= */ 0);
+ // // TEST HELPERS
- assertArrayEquals(new Integer[] {10}, preState.findStartedUsers(postState).toArray());
- assertArrayEquals(new Integer[] {}, preState.findStoppedUsers(postState).toArray());
- }
+ /** Return a device with the user state calls mocked. */
+ private ITestDevice mockDeviceUserState(int currentUser, Integer[] userIds) throws Exception {
+ Integer[] flags = new Integer[userIds.length];
+ Arrays.fill(flags, 0);
- @Test
- public void testFindStopedUsers() throws Exception {
- DeviceUserState preState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0, 10},
- /* runningUsers= */ new Integer[] {0, 10},
- /* currentUser= */ 0);
- DeviceUserState postState =
- getMockedUserState(
- /* userIds= */ new Integer[] {0, 10},
- /* runningUsers= */ new Integer[] {0},
- /* currentUser= */ 0);
+ Boolean[] isRunning = new Boolean[userIds.length];
+ Arrays.fill(isRunning, false);
+ isRunning[0] = true;
- assertArrayEquals(new Integer[] {}, preState.findStartedUsers(postState).toArray());
- assertArrayEquals(new Integer[] {10}, preState.findStoppedUsers(postState).toArray());
- }
-
- // TEST HELPERS
-
- /** Return an instantiated DeviceUserState which was mocked. */
- private DeviceUserState getMockedUserState(
- Integer[] userIds, Integer[] runningUsers, int currentUser) throws Exception {
- ITestDevice device = mockDeviceUserState(userIds, runningUsers, currentUser);
- return new UserChecker.DeviceUserState(device);
+ return mockDeviceUserState(currentUser, userIds, flags, isRunning);
}
/** Return a device with the user state calls mocked. */
private ITestDevice mockDeviceUserState(
- Integer[] userIds, Integer[] runningUsers, int currentUser) throws Exception {
- HashSet<Integer> runningUsersSet = new HashSet<Integer>(Arrays.asList(runningUsers));
+ int currentUser, Integer[] userIds, Integer[] flags, Boolean[] isRunning)
+ throws Exception {
ITestDevice device = mock(ITestDevice.class);
+
when(device.getCurrentUser()).thenReturn(currentUser);
- mockListUsers(device, userIds);
- for (int userId : userIds) {
- when(device.isUserRunning(userId)).thenReturn(runningUsersSet.contains(userId));
- }
+ mockListUsersInfo(device, userIds, flags, isRunning);
return device;
}
- private void mockListUsers(ITestDevice device, Integer[] userIds) throws Exception {
- when(device.listUsers()).thenReturn(new ArrayList<Integer>(Arrays.asList(userIds)));
+ private void mockListUsersInfo(
+ ITestDevice device, Integer[] userIds, Integer[] flags, Boolean[] isRunning)
+ throws Exception {
+ Map<Integer, UserInfo> result = new HashMap<>();
+ for (int i = 0; i < userIds.length; i++) {
+ int userId = userIds[i];
+ result.put(
+ userId,
+ new UserInfo(
+ /* userId= */ userId,
+ /* userName= */ "usr" + userId,
+ /* flag= */ flags[i],
+ /* isRunning= */ isRunning[i]));
+ }
+ when(device.getUserInfos()).thenReturn(result);
}
}
diff --git a/tests/src/com/android/tradefed/targetprep/AppSetupTest.java b/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
index 8e5e5ab..870fadf 100644
--- a/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
+++ b/tests/src/com/android/tradefed/targetprep/AppSetupTest.java
@@ -21,7 +21,6 @@
import static org.mockito.Mockito.times;
import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.build.IAppBuildInfo;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.VersionedFile;
import com.android.tradefed.config.OptionSetter;
@@ -30,20 +29,22 @@
import com.android.tradefed.util.AaptParser;
import com.android.tradefed.util.FileUtil;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
import org.easymock.EasyMock;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
/**
* Unit Tests for {@link AppSetup}.
*/
@@ -53,17 +54,28 @@
private static final String SERIAL = "serial";
private AppSetup mAppSetup;
private ITestDevice mMockDevice;
- private IAppBuildInfo mMockBuildInfo;
+ private IBuildInfo mMockBuildInfo;
private AaptParser mMockAaptParser;
+ private List<VersionedFile> mApps;
@Before
- public void setUp() {
+ public void setUp() throws IOException {
mAppSetup = new AppSetup();
mMockDevice = EasyMock.createMock(ITestDevice.class);
EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn(SERIAL);
EasyMock.expect(mMockDevice.getDeviceDescriptor()).andStubReturn(null);
- mMockBuildInfo = EasyMock.createMock(IAppBuildInfo.class);
+ mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
mMockAaptParser = Mockito.mock(AaptParser.class);
+ mApps = new ArrayList<>();
+ File tmpFile = FileUtil.createTempFile("versioned", ".test");
+ mApps.add(new VersionedFile(tmpFile, "1"));
+ }
+
+ @After
+ public void tearDown() {
+ for (VersionedFile f : mApps) {
+ FileUtil.deleteFile(f.getFile());
+ }
}
private void replayMocks() {
@@ -75,18 +87,14 @@
}
/**
- * Test for {@link AppSetup#setUp(ITestDevice, IBuildInfo)} when the IBuildInfo is not an
- * instance of {@link IAppBuildInfo}.
+ * Test for {@link AppSetup#setUp(ITestDevice, IBuildInfo)} when the IBuildInfo doesn't contain
+ * any apps.
*/
@Test
- public void testSetup_notIAppBuildInfo() throws Exception {
+ public void testSetup_notApps() throws Exception {
replayMocks();
- try {
- mAppSetup.setUp(mMockDevice, new BuildInfo());
- fail("Should have thrown an exception.");
- } catch (IllegalArgumentException expected) {
- assertEquals("Provided buildInfo is not a AppBuildInfo", expected.getMessage());
- }
+ // Inop setup
+ mAppSetup.setUp(mMockDevice, new BuildInfo());
verifyMocks();
}
@@ -274,6 +282,7 @@
*/
@Test
public void testSetup_executePostInstall() throws Exception {
+ EasyMock.expect(mMockBuildInfo.getAppPackageFiles()).andReturn(mApps);
final String fakeCmd = "fake command";
OptionSetter setter = new OptionSetter(mAppSetup);
setter.setOptionValue("install", "false");
@@ -292,6 +301,7 @@
*/
@Test
public void testSetup_uninstallAll_noPackage() throws Exception {
+ EasyMock.expect(mMockBuildInfo.getAppPackageFiles()).andReturn(mApps);
OptionSetter setter = new OptionSetter(mAppSetup);
setter.setOptionValue("install", "false");
setter.setOptionValue("uninstall-all", "true");
@@ -308,6 +318,7 @@
*/
@Test
public void testSetup_uninstallAll() throws Exception {
+ EasyMock.expect(mMockBuildInfo.getAppPackageFiles()).andReturn(mApps);
OptionSetter setter = new OptionSetter(mAppSetup);
setter.setOptionValue("install", "false");
setter.setOptionValue("uninstall-all", "true");
@@ -326,6 +337,7 @@
*/
@Test
public void testSetup_uninstallAll_fails() throws Exception {
+ EasyMock.expect(mMockBuildInfo.getAppPackageFiles()).andReturn(mApps);
OptionSetter setter = new OptionSetter(mAppSetup);
setter.setOptionValue("install", "false");
setter.setOptionValue("uninstall-all", "true");
diff --git a/tests/src/com/android/tradefed/targetprep/CreateUserPreparerTest.java b/tests/src/com/android/tradefed/targetprep/CreateUserPreparerTest.java
index b96d6bd..a7cfaef 100644
--- a/tests/src/com/android/tradefed/targetprep/CreateUserPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/CreateUserPreparerTest.java
@@ -48,6 +48,7 @@
doReturn(10).when(mMockDevice).getCurrentUser();
doReturn(5).when(mMockDevice).createUser(Mockito.any());
doReturn(true).when(mMockDevice).switchUser(5);
+ doReturn(true).when(mMockDevice).startUser(5, true);
mPreparer.setUp(mMockDevice, null);
doReturn(true).when(mMockDevice).removeUser(5);
diff --git a/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java b/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
index 42618f2..03b5aa1 100644
--- a/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/DeviceFlashPreparerTest.java
@@ -115,6 +115,7 @@
@Test
public void testSetup() throws Exception {
doSetupExpectations();
+ mMockFlasher.setShouldFlashRamdisk(false);
// report flashing success in normal case
EasyMock.expect(mMockFlasher.getSystemFlashingStatus())
.andReturn(CommandStatus.SUCCESS).anyTimes();
@@ -161,6 +162,21 @@
}
}
+ /**
+ * Test {@link DeviceFlashPreparer#setUp(ITestDevice, IBuildInfo)} when ramdisk flashing is
+ * required via parameter but not provided in build info
+ */
+ @Test
+ public void testSetUp_noRamdisk() throws Exception {
+ try {
+ mDeviceFlashPreparer.setShouldFlashRamdisk(true);
+ mDeviceFlashPreparer.setUp(mMockDevice, mMockBuildInfo);
+ fail("IllegalArgumentException not thrown");
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
/** Test {@link DeviceFlashPreparer#setUp(ITestDevice, IBuildInfo)} when build does not boot. */
@Test
public void testSetup_buildError() throws Exception {
@@ -168,6 +184,7 @@
mMockFlasher.overrideDeviceOptions(mMockDevice);
mMockFlasher.setForceSystemFlash(false);
mMockFlasher.setDataWipeSkipList(Arrays.asList(new String[]{}));
+ mMockFlasher.setShouldFlashRamdisk(false);
mMockFlasher.flash(mMockDevice, mMockBuildInfo);
mMockFlasher.setWipeTimeout(EasyMock.anyLong());
mMockDevice.waitForDeviceOnline();
@@ -211,6 +228,7 @@
mMockFlasher.overrideDeviceOptions(mMockDevice);
mMockFlasher.setForceSystemFlash(false);
mMockFlasher.setDataWipeSkipList(Arrays.asList(new String[]{}));
+ mMockFlasher.setShouldFlashRamdisk(false);
mMockFlasher.flash(mMockDevice, mMockBuildInfo);
EasyMock.expectLastCall().andThrow(new DeviceNotAvailableException());
mMockFlasher.setWipeTimeout(EasyMock.anyLong());
@@ -236,6 +254,7 @@
@Test
public void testSetup_flashSkipped() throws Exception {
doSetupExpectations();
+ mMockFlasher.setShouldFlashRamdisk(false);
// report flashing status as null (for not flashing system partitions)
EasyMock.expect(mMockFlasher.getSystemFlashingStatus()).andReturn(null).anyTimes();
EasyMock.replay(mMockFlasher, mMockDevice);
@@ -243,4 +262,25 @@
EasyMock.verify(mMockFlasher, mMockDevice);
assertFalse("should not report flashing metrics in normal case", mFlashingMetricsReported);
}
+
+ /**
+ * Verifies that the ramdisk flashing parameter is passed down to the device flasher
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSetup_flashRamdisk() throws Exception {
+ mDeviceFlashPreparer.setShouldFlashRamdisk(true);
+ mMockBuildInfo.setRamdiskFile(new File("foo"), "0");
+ doSetupExpectations();
+ // report flashing success in normal case
+ EasyMock.expect(mMockFlasher.getSystemFlashingStatus())
+ .andReturn(CommandStatus.SUCCESS)
+ .anyTimes();
+ mMockFlasher.setShouldFlashRamdisk(true);
+ EasyMock.expectLastCall();
+ EasyMock.replay(mMockFlasher, mMockDevice);
+ mDeviceFlashPreparer.setUp(mMockDevice, mMockBuildInfo);
+ EasyMock.verify(mMockFlasher, mMockDevice);
+ }
}
diff --git a/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java b/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
index 3f228d3..215f623 100644
--- a/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
+++ b/tests/src/com/android/tradefed/targetprep/FastbootDeviceFlasherTest.java
@@ -590,6 +590,90 @@
}
/**
+ * Test the fastboot flashing with ramdisk interaction flow
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testFlashingSystemWithRamdisk() throws Exception {
+ final String buildId = "systemBuildId";
+ mFlasher.setShouldFlashRamdisk(true);
+ IDeviceBuildInfo mockBuild = EasyMock.createMock(IDeviceBuildInfo.class);
+ EasyMock.expect(mockBuild.getDeviceBuildId()).andReturn(buildId);
+ File deviceImage = FileUtil.createTempFile("fakeDeviceImage", "");
+ File ramdisk = FileUtil.createTempFile("fakeRamdisk", "");
+ EasyMock.expect(mockBuild.getRamdiskFile()).andReturn(ramdisk);
+ try {
+ EasyMock.expect(mockBuild.getDeviceImageFile()).andStubReturn(deviceImage);
+ CommandResult res = new CommandResult(CommandStatus.SUCCESS);
+ res.setStderr("flashing");
+ EasyMock.expect(
+ mMockDevice.executeLongFastbootCommand(
+ EasyMock.eq("--skip-reboot"),
+ EasyMock.eq("update"),
+ EasyMock.eq(deviceImage.getAbsolutePath())))
+ .andReturn(res);
+ EasyMock.expect(
+ mMockDevice.executeLongFastbootCommand(
+ EasyMock.eq("flash"),
+ EasyMock.eq("boot"),
+ EasyMock.eq(ramdisk.getAbsolutePath())))
+ .andReturn(res);
+ mMockDevice.reboot();
+ EasyMock.expectLastCall();
+ EasyMock.replay(mMockDevice, mockBuild);
+ assertTrue(mFlasher.checkAndFlashSystem(mMockDevice, buildId, null, mockBuild));
+ EasyMock.verify(mMockDevice, mockBuild);
+ assertEquals(
+ "system flashing status should be \"SUCCESS\"",
+ CommandStatus.SUCCESS,
+ mFlasher.getSystemFlashingStatus());
+ } finally {
+ FileUtil.deleteFile(deviceImage);
+ FileUtil.deleteFile(ramdisk);
+ }
+ }
+
+ /**
+ * Test that ramdisk is still flashed even system partition flashing is skipped
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSkipFlashingSystemWithRamdisk() throws Exception {
+ final String buildId = "systemBuildId";
+ final String buildFlavor = "systemBuildFlavor";
+ mFlasher.setShouldFlashRamdisk(true);
+ IDeviceBuildInfo mockBuild = EasyMock.createMock(IDeviceBuildInfo.class);
+ File ramdisk = FileUtil.createTempFile("fakeRamdisk", "");
+ EasyMock.expect(mockBuild.getRamdiskFile()).andReturn(ramdisk);
+ try {
+ EasyMock.expect(mockBuild.getDeviceBuildId()).andReturn(buildId);
+ EasyMock.expect(mockBuild.getBuildFlavor()).andReturn(buildFlavor);
+ mMockDevice.rebootUntilOnline();
+ EasyMock.expectLastCall();
+ CommandResult res = new CommandResult(CommandStatus.SUCCESS);
+ res.setStderr("flashing");
+ EasyMock.expect(
+ mMockDevice.executeLongFastbootCommand(
+ EasyMock.eq("flash"),
+ EasyMock.eq("boot"),
+ EasyMock.eq(ramdisk.getAbsolutePath())))
+ .andReturn(res);
+ mMockDevice.reboot();
+ EasyMock.expectLastCall();
+ EasyMock.replay(mMockDevice, mockBuild);
+ assertFalse(mFlasher.checkAndFlashSystem(mMockDevice, buildId, buildFlavor, mockBuild));
+ EasyMock.verify(mMockDevice, mockBuild);
+ assertNull(
+ "system flash status should be null when partitions are not flashed",
+ mFlasher.getSystemFlashingStatus());
+ } finally {
+ FileUtil.deleteFile(ramdisk);
+ }
+ }
+
+ /**
* Test {@link FastbootDeviceFlasher#checkAndFlashSystem(ITestDevice, String, String,
* IDeviceBuildInfo)} with flash options
*/
diff --git a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
index 869577a..4cde810 100644
--- a/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/InstallApexModuleTargetPreparerTest.java
@@ -55,12 +55,16 @@
private BundletoolUtil mMockBundletoolUtil;
private File mFakeApex;
private File mFakeApk;
+ private File mFakeApk2;
+ private File mFakePersistentApk;
private File mFakeApkApks;
private File mFakeApexApks;
private File mBundletoolJar;
private OptionSetter mSetter;
private static final String APEX_PACKAGE_NAME = "com.android.FAKE_APEX_PACKAGE_NAME";
private static final String APK_PACKAGE_NAME = "com.android.FAKE_APK_PACKAGE_NAME";
+ private static final String APK2_PACKAGE_NAME = "com.android.FAKE_APK2_PACKAGE_NAME";
+ private static final String PERSISTENT_APK_PACKAGE_NAME = "com.android.PERSISTENT_PACKAGE_NAME";
private static final String SPLIT_APEX_PACKAGE_NAME =
"com.android.SPLIT_FAKE_APEX_PACKAGE_NAME";
private static final String SPLIT_APK_PACKAGE_NAME =
@@ -69,17 +73,22 @@
private static final long APEX_VERSION = 1;
private static final String APEX_NAME = "fakeApex.apex";
private static final String APK_NAME = "fakeApk.apk";
+ private static final String APK2_NAME = "fakeSecondApk.apk";
+ private static final String PERSISTENT_APK_NAME = "fakePersistentApk.apk";
private static final String SPLIT_APEX_APKS_NAME = "fakeApex.apks";
private static final String SPLIT_APK__APKS_NAME = "fakeApk.apks";
private static final String BUNDLETOOL_JAR_NAME = "bundletool.jar";
private static final String APEX_DATA_DIR = "/data/apex/active/";
private static final String STAGING_DATA_DIR = "/data/app-staging/";
private static final String SESSION_DATA_DIR = "/data/apex/sessions/";
+ private static final String APEX_STAGING_WAIT_TIME = "10";
@Before
public void setUp() throws Exception {
mFakeApex = FileUtil.createTempFile("fakeApex", ".apex");
mFakeApk = FileUtil.createTempFile("fakeApk", ".apk");
+ mFakeApk2 = FileUtil.createTempFile("fakeSecondApk", ".apk");
+ mFakePersistentApk = FileUtil.createTempFile("fakePersistentApk", ".apk");
mMockDevice = EasyMock.createMock(ITestDevice.class);
mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
mMockBundletoolUtil = Mockito.mock(BundletoolUtil.class);
@@ -111,7 +120,13 @@
return mFakeApex;
}
if (appFileName.endsWith(".apk")) {
- return mFakeApk;
+ if (appFileName.contains("Second")) {
+ return mFakeApk2;
+ } else if (appFileName.contains("Persistent")) {
+ return mFakePersistentApk;
+ } else {
+ return mFakeApk;
+ }
}
if (appFileName.endsWith(".apks")) {
if (appFileName.contains("Apex")) {
@@ -133,12 +148,18 @@
if (testAppFile.getName().endsWith(".apex")) {
return APEX_PACKAGE_NAME;
}
- if (testAppFile.getName().endsWith(".apk") &&
- !testAppFile.getName().contains("Split")) {
- return APK_PACKAGE_NAME;
+ if (testAppFile.getName().endsWith(".apk")
+ && !testAppFile.getName().contains("Split")) {
+ if (testAppFile.getName().contains("Second")) {
+ return APK2_PACKAGE_NAME;
+ } else if (testAppFile.getName().contains("Persistent")) {
+ return PERSISTENT_APK_PACKAGE_NAME;
+ } else {
+ return APK_PACKAGE_NAME;
+ }
}
- if (testAppFile.getName().endsWith(".apk") &&
- testAppFile.getName().contains("Split")) {
+ if (testAppFile.getName().endsWith(".apk")
+ && testAppFile.getName().contains("Split")) {
return SPLIT_APK_PACKAGE_NAME;
}
return null;
@@ -155,16 +176,29 @@
}
return apexInfo;
}
+
+ @Override
+ protected boolean isPersistentApk(
+ String filename, ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError {
+ if (filename.contains("Persistent")) {
+ return true;
+ }
+ return false;
+ }
};
mSetter = new OptionSetter(mInstallApexModuleTargetPreparer);
mSetter.setOptionValue("cleanup-apks", "true");
+ mSetter.setOptionValue("apex-staging-wait-time", APEX_STAGING_WAIT_TIME);
}
@After
public void tearDown() throws Exception {
FileUtil.deleteFile(mFakeApex);
FileUtil.deleteFile(mFakeApk);
+ FileUtil.deleteFile(mFakeApk2);
+ FileUtil.deleteFile(mFakePersistentApk);
}
@Test
@@ -183,7 +217,7 @@
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
mMockDevice.reboot();
EasyMock.expectLastCall();
- mockSuccessfulInstallPackageAndReboot();
+ mockSuccessfulInstallPackageAndReboot(mFakeApex);
Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -201,7 +235,7 @@
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
- mockSuccessfulInstallPackageAndReboot();
+ mockSuccessfulInstallPackageAndReboot(mFakeApex);
Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -226,7 +260,7 @@
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
mMockDevice.reboot();
EasyMock.expectLastCall();
- mockSuccessfulInstallPackageAndReboot();
+ mockSuccessfulInstallPackageAndReboot(mFakeApex);
Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -252,7 +286,7 @@
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
mMockDevice.reboot();
EasyMock.expectLastCall();
- mockSuccessfulInstallPackageAndReboot();
+ mockSuccessfulInstallPackageAndReboot(mFakeApex);
EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(new HashSet<ApexInfo>());
try {
@@ -284,7 +318,7 @@
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
mMockDevice.reboot();
EasyMock.expectLastCall();
- mockSuccessfulInstallPackageAndReboot();
+ mockSuccessfulInstallPackageAndReboot(mFakeApex);
Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME_TO_FAIL", 1));
EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -319,8 +353,11 @@
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
mMockDevice.reboot();
EasyMock.expectLastCall();
- EasyMock.expect(mMockDevice.installPackage((File) EasyMock.anyObject(), EasyMock.eq(true)))
- .andReturn(null)
+ List<String> trainInstallCmd = new ArrayList<>();
+ trainInstallCmd.add("install-multi-package");
+ trainInstallCmd.add(mFakeApk.getAbsolutePath());
+ EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+ .andReturn("Success")
.once();
EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
@@ -331,6 +368,75 @@
}
@Test
+ public void testSetupAndTearDown_InstallMultipleApk() throws Exception {
+ mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
+ mInstallApexModuleTargetPreparer.addTestFileName(APK2_NAME);
+ mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+ EasyMock.expectLastCall().times(1);
+ mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+ EasyMock.expectLastCall().times(1);
+ mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+ EasyMock.expectLastCall().times(1);
+ CommandResult res = new CommandResult();
+ res.setStdout("test.apex");
+ EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
+ EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
+ EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
+ mMockDevice.reboot();
+ EasyMock.expectLastCall();
+ List<File> apks = new ArrayList<>();
+ apks.add(mFakeApk);
+ apks.add(mFakeApk2);
+ mockSuccessfulInstallMultiApkWithoutReboot(apks);
+ EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
+ EasyMock.expect(mMockDevice.uninstallPackage(APK2_PACKAGE_NAME)).andReturn(null).once();
+
+ EasyMock.replay(mMockBuildInfo, mMockDevice);
+ mInstallApexModuleTargetPreparer.setUp(mMockDevice, mMockBuildInfo);
+ mInstallApexModuleTargetPreparer.tearDown(mMockDevice, mMockBuildInfo, null);
+ EasyMock.verify(mMockBuildInfo, mMockDevice);
+ }
+
+ @Test
+ public void testSetupAndTearDown_InstallMultipleApkContainingPersistentApk() throws Exception {
+ mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
+ mInstallApexModuleTargetPreparer.addTestFileName(APK2_NAME);
+ mInstallApexModuleTargetPreparer.addTestFileName(PERSISTENT_APK_NAME);
+ mMockDevice.deleteFile(APEX_DATA_DIR + "*");
+ EasyMock.expectLastCall().times(1);
+ mMockDevice.deleteFile(SESSION_DATA_DIR + "*");
+ EasyMock.expectLastCall().times(1);
+ mMockDevice.deleteFile(STAGING_DATA_DIR + "*");
+ EasyMock.expectLastCall().times(1);
+ CommandResult res = new CommandResult();
+ res.setStdout("test.apex");
+ EasyMock.expect(mMockDevice.executeShellV2Command("ls " + APEX_DATA_DIR)).andReturn(res);
+ EasyMock.expect(mMockDevice.executeShellV2Command("ls " + SESSION_DATA_DIR)).andReturn(res);
+ EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
+ mMockDevice.reboot();
+ EasyMock.expectLastCall();
+ List<String> trainInstallCmd = new ArrayList<>();
+ trainInstallCmd.add("install-multi-package");
+ trainInstallCmd.add("--staged");
+ trainInstallCmd.add(mFakeApk.getAbsolutePath());
+ trainInstallCmd.add(mFakeApk2.getAbsolutePath());
+ trainInstallCmd.add(mFakePersistentApk.getAbsolutePath());
+ EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+ .andReturn("Success")
+ .once();
+ mMockDevice.reboot();
+ EasyMock.expect(mMockDevice.uninstallPackage(APK_PACKAGE_NAME)).andReturn(null).once();
+ EasyMock.expect(mMockDevice.uninstallPackage(APK2_PACKAGE_NAME)).andReturn(null).once();
+ EasyMock.expect(mMockDevice.uninstallPackage(PERSISTENT_APK_PACKAGE_NAME))
+ .andReturn(null)
+ .once();
+ EasyMock.replay(mMockBuildInfo, mMockDevice);
+ mInstallApexModuleTargetPreparer.setUp(mMockDevice, mMockBuildInfo);
+ mInstallApexModuleTargetPreparer.tearDown(mMockDevice, mMockBuildInfo, null);
+ EasyMock.verify(mMockBuildInfo, mMockDevice);
+ }
+
+ @Test
public void testSetupAndTearDown_ApkAndApks() throws Exception {
mInstallApexModuleTargetPreparer.addTestFileName(APK_NAME);
mInstallApexModuleTargetPreparer.addTestFileName(SPLIT_APK__APKS_NAME);;
@@ -434,7 +540,7 @@
EasyMock.expect(mMockDevice.executeShellV2Command("ls " + STAGING_DATA_DIR)).andReturn(res);
mMockDevice.reboot();
EasyMock.expectLastCall();
- mockSuccessfulInstallPackageAndReboot();
+ mockSuccessfulInstallPackageAndReboot(mFakeApex);
Set<ApexInfo> activatedApex = new HashSet<ApexInfo>();
activatedApex.add(new ApexInfo("com.android.FAKE_APEX_PACKAGE_NAME", 1));
EasyMock.expect(mMockDevice.getActiveApexes()).andReturn(activatedApex);
@@ -608,14 +714,36 @@
}
}
- private void mockSuccessfulInstallPackageAndReboot() throws Exception {
- EasyMock.expect(mMockDevice.installPackage((File) EasyMock.anyObject(), EasyMock.eq(true)))
- .andReturn(null)
+ /** Test that teardown without setup does not cause a NPE. */
+ @Test
+ public void testTearDown() throws Exception {
+ EasyMock.replay(mMockBuildInfo, mMockDevice);
+ mInstallApexModuleTargetPreparer.tearDown(mMockDevice, mMockBuildInfo, null);
+ EasyMock.verify(mMockBuildInfo, mMockDevice);
+ }
+
+ private void mockSuccessfulInstallPackageAndReboot(File f) throws Exception {
+ List<String> trainInstallCmd = new ArrayList<>();
+ trainInstallCmd.add("install-multi-package");
+ trainInstallCmd.add(f.getAbsolutePath());
+ EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+ .andReturn("Success")
.once();
mMockDevice.reboot();
EasyMock.expectLastCall().once();
}
+ private void mockSuccessfulInstallMultiApkWithoutReboot(List<File> apks) throws Exception {
+ List<String> trainInstallCmd = new ArrayList<>();
+ trainInstallCmd.add("install-multi-package");
+ for (File apk : apks) {
+ trainInstallCmd.add(apk.getAbsolutePath());
+ }
+ EasyMock.expect(mMockDevice.executeAdbCommand(trainInstallCmd.toArray(new String[0])))
+ .andReturn("Success")
+ .once();
+ }
+
private void mockSuccessfulInstallMultiPackageAndReboot() throws Exception {
List<String> trainInstallCmd = new ArrayList<>();
trainInstallCmd.add("install-multi-package");
diff --git a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
index b5c9a20..3817c98 100644
--- a/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/PushFilePreparerTest.java
@@ -19,7 +19,6 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.tradefed.build.BuildInfo;
@@ -74,7 +73,7 @@
@Test
public void testLocalNoExist() throws Exception {
- mOptionSetter.setOptionValue("push", "/noexist->/data/");
+ mOptionSetter.setOptionValue("push-file", "/noexist", "/data/");
mOptionSetter.setOptionValue("post-push", "ls /");
EasyMock.replay(mMockDevice);
try {
@@ -89,7 +88,7 @@
@Test
public void testRemoteNoExist() throws Exception {
- mOptionSetter.setOptionValue("push", "/bin/sh->/noexist/");
+ mOptionSetter.setOptionValue("push-file", "/bin/sh", "/noexist/");
mOptionSetter.setOptionValue("post-push", "ls /");
// expect a pushFile() call and return false (failed)
EasyMock.expect(
@@ -118,7 +117,7 @@
File testFile = new File(testsDir, "perf_test");
testFile.createNewFile();
info.setFile("perf_test", testFile, "v1");
- mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/");
+ mOptionSetter.setOptionValue("push-file", "perf_test", "/data/local/tmp/");
// expect a pushFile() to be done with the appended file name.
EasyMock.expect(
mMockDevice.pushFile(
@@ -141,7 +140,7 @@
File testFile = new File(testsDir, "perf_test");
testFile.mkdir();
info.setFile("perf_test", testFile, "v1");
- mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/");
+ mOptionSetter.setOptionValue("push-file", "perf_test", "/data/local/tmp/");
EasyMock.expect(mMockDevice.doesFileExist("/data/local/tmp/")).andReturn(true);
EasyMock.expect(mMockDevice.isDirectory("/data/local/tmp/")).andReturn(true);
// expect a pushFile() to be done with the appended file name.
@@ -168,7 +167,7 @@
File testFile = new File(testsDir, "perf_test");
testFile.mkdir();
info.setFile("perf_test", testFile, "v1");
- mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/file");
+ mOptionSetter.setOptionValue("push-file", "perf_test", "/data/local/tmp/file");
EasyMock.expect(mMockDevice.doesFileExist("/data/local/tmp/file")).andReturn(true);
EasyMock.expect(mMockDevice.isDirectory("/data/local/tmp/file")).andReturn(false);
EasyMock.replay(mMockDevice);
@@ -185,11 +184,11 @@
}
/**
- * Test pushing a file to remote dir. The 'push' contract allows to push the file to a named
- * directory.
+ * Test pushing a file to remote dir. If there are multiple files push to the same place, the
+ * latest win.
*/
@Test
- public void testRemotePush_conflict() throws Exception {
+ public void testRemotePush_override() throws Exception {
BuildInfo info = new BuildInfo();
File testsDir = FileUtil.createTempDir("tests_dir");
try {
@@ -199,27 +198,50 @@
testFile2.createNewFile();
info.setFile("perf_test", testFile, "v1");
info.setFile("perf_test2", testFile2, "v1");
- mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/perf_test");
- mOptionSetter.setOptionValue("push", "perf_test2->/data/local/tmp/perf_test");
+ mOptionSetter.setOptionValue("push-file", "perf_test", "/data/local/tmp/perf_test");
+ mOptionSetter.setOptionValue("push-file", "perf_test2", "/data/local/tmp/perf_test");
EasyMock.expect(mMockDevice.isDirectory(EasyMock.anyObject())).andStubReturn(false);
- // expect a pushFile() to be done with the appended file name.
- EasyMock.expect(
- mMockDevice.pushFile(
- EasyMock.eq(testFile),
- EasyMock.eq("/data/local/tmp/perf_test")))
- .andReturn(Boolean.TRUE);
+ // the latest config win.
EasyMock.expect(
mMockDevice.pushFile(
EasyMock.eq(testFile2),
EasyMock.eq("/data/local/tmp/perf_test")))
.andReturn(Boolean.TRUE);
EasyMock.replay(mMockDevice);
- try {
- mPreparer.setUp(mMockDevice, info);
- fail("Should have thrown an exception.");
- } catch (TargetSetupError expected) {
- assertTrue(expected.getMessage().contains("We pushed two files to the "));
- }
+ mPreparer.setUp(mMockDevice, info);
+ EasyMock.verify(mMockDevice);
+ } finally {
+ FileUtil.recursiveDelete(testsDir);
+ }
+ }
+
+ /**
+ * Test pushing a file to remote dir. If both push and push-file push to the same remote file,
+ * the push-file win.
+ */
+ @Test
+ public void testPushFileAndPush_override() throws Exception {
+ BuildInfo info = new BuildInfo();
+ File testsDir = FileUtil.createTempDir("tests_dir");
+ try {
+ File testFile = new File(testsDir, "perf_test");
+ testFile.createNewFile();
+ File testFile2 = new File(testsDir, "perf_test2");
+ testFile2.createNewFile();
+ info.setFile("perf_test", testFile, "v1");
+ info.setFile("perf_test2", testFile2, "v1");
+
+ mOptionSetter.setOptionValue("push-file", "perf_test2", "/data/local/tmp/perf_test");
+ mOptionSetter.setOptionValue("push", "perf_test->/data/local/tmp/perf_test");
+ EasyMock.expect(mMockDevice.isDirectory(EasyMock.anyObject())).andStubReturn(false);
+ // the latest config win.
+ EasyMock.expect(
+ mMockDevice.pushFile(
+ EasyMock.eq(testFile2),
+ EasyMock.eq("/data/local/tmp/perf_test")))
+ .andReturn(Boolean.TRUE);
+ EasyMock.replay(mMockDevice);
+ mPreparer.setUp(mMockDevice, info);
EasyMock.verify(mMockDevice);
} finally {
FileUtil.recursiveDelete(testsDir);
@@ -599,6 +621,45 @@
}
/**
+ * Ensure that in case we don't find the module directory. We fallback to top level match first
+ * and not first found.
+ */
+ @Test
+ public void testPush_moduleName_noMatch() throws Exception {
+ mOptionSetter.setOptionValue("push", "lib64->/data/local/tmp/lib");
+ mPreparer.setAbi(new Abi("x86_64", "64"));
+
+ mPreparer.setInvocationContext(createModuleWithName("CtsBionicTestCases"));
+ IDeviceBuildInfo info = new DeviceBuildInfo();
+ File tmpFolder = FileUtil.createTempDir("push-file-tests-dir");
+ try {
+ File beforeName = new File(tmpFolder, "lib64");
+ FileUtil.mkdirsRWX(beforeName);
+ File libX86File = new File(tmpFolder, "DATA/lib64");
+ FileUtil.mkdirsRWX(libX86File);
+ info.setFile(BuildInfoFileKey.TESTDIR_IMAGE, tmpFolder, "v1");
+ EasyMock.expect(mMockDevice.doesFileExist("/data/local/tmp/lib")).andReturn(false);
+ EasyMock.expect(mMockDevice.executeShellCommand("mkdir -p \"/data/local/tmp/lib\""))
+ .andReturn("");
+ Capture<Set<String>> capture = new Capture<>();
+ EasyMock.expect(
+ mMockDevice.pushDir(
+ EasyMock.eq(new File(tmpFolder, "lib64")),
+ EasyMock.eq("/data/local/tmp/lib"),
+ EasyMock.capture(capture)))
+ .andReturn(true);
+ EasyMock.replay(mMockDevice);
+ mPreparer.setUp(mMockDevice, info);
+ EasyMock.verify(mMockDevice);
+ // The x86 folder was not filtered
+ Set<String> capValue = capture.getValue();
+ assertFalse(capValue.contains("x86_64"));
+ } finally {
+ FileUtil.recursiveDelete(tmpFolder);
+ }
+ }
+
+ /**
* Test that if a binary name is repeating in another directory that is searched first we don't
* use it and do prioritize the module name directory.
*/
diff --git a/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
index 70187a2..c5bbb06 100644
--- a/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
+++ b/tests/src/com/android/tradefed/targetprep/SwitchUserTargetPreparerTest.java
@@ -27,6 +27,7 @@
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.UserInfo;
import org.junit.Before;
import org.junit.Test;
@@ -35,6 +36,9 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Map;
+import java.util.HashMap;
+
/** Unit tests for {@link SwitchUserTargetPreparer}. */
@RunWith(JUnit4.class)
public class SwitchUserTargetPreparerTest {
@@ -53,26 +57,97 @@
}
@Test
- public void testSetUpRunAsPrimary_ifAlreadyInPrimary_switchToPrimary()
+ public void testSetUpRunAsPrimary_ifAlreadyInPrimary_noSwitch()
throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
- // setup
- mockUsers(/* primaryUserId= */ 11, /* currentUserId= */ 11);
mOptionSetter.setOptionValue("user-type", "primary");
+ // setup
+ when(mMockDevice.getCurrentUser()).thenReturn(11);
+ mockListUsersInfo(
+ mMockDevice,
+ /* userIds= */ new Integer[] {0, 11},
+ /* flags= */ new Integer[] {0, UserInfo.FLAG_PRIMARY});
+
+
// act
mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
// assert
- verify(mMockDevice, times(1)).switchUser(11);
+ verify(mMockDevice, never()).switchUser(anyInt());
}
@Test
- public void testSetUpRunAsSystem_ifAlreadyInSystem_switchToSystem()
+ public void testSetUpRunAsSystem_ifAlreadyInSystem_noSwitch()
throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
- // setup
- mockUsers(/* primaryUserId= */ 11, /* currentUserId= */ USER_SYSTEM);
mOptionSetter.setOptionValue("user-type", "system");
+ // setup
+ when(mMockDevice.getCurrentUser()).thenReturn(0);
+ mockListUsersInfo(
+ mMockDevice,
+ /* userIds= */ new Integer[] {0, 11},
+ /* flags= */ new Integer[] {0, UserInfo.FLAG_PRIMARY});
+
+ // act
+ mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
+
+ // assert
+ verify(mMockDevice, never()).switchUser(0);
+ }
+
+ @Test
+ public void testSetUpRunAsPrimary_ifNotInPrimary_switchToPrimary()
+ throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
+ mOptionSetter.setOptionValue("user-type", "primary");
+
+ // setup
+ when(mMockDevice.getCurrentUser()).thenReturn(11);
+ mockListUsersInfo(
+ mMockDevice,
+ /* userIds= */ new Integer[] {0, 10, 11},
+ /* flags= */ new Integer[] {0, UserInfo.FLAG_PRIMARY, 0});
+ when(mMockDevice.switchUser(10)).thenReturn(true);
+
+ // act
+ mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
+
+ // assert
+ verify(mMockDevice, times(1)).switchUser(10);
+ }
+
+ @Test
+ public void testSetUpRunAsGuest_ifNotInGuest_switchToGuest()
+ throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
+ mOptionSetter.setOptionValue("user-type", "guest");
+
+ // setup
+ when(mMockDevice.getCurrentUser()).thenReturn(11);
+ mockListUsersInfo(
+ mMockDevice,
+ /* userIds= */ new Integer[] {0, 10, 11},
+ /* flags= */ new Integer[] {0, UserInfo.FLAG_GUEST, 0});
+ when(mMockDevice.switchUser(10)).thenReturn(true);
+
+ // act
+ mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
+
+ // assert
+ verify(mMockDevice, times(1)).switchUser(10);
+ }
+
+ @Test
+ public void testSetUpRunAsSystem_ifNotInSystem_switchToSystem()
+ throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
+ mOptionSetter.setOptionValue("user-type", "system");
+
+ // setup
+ when(mMockDevice.getCurrentUser()).thenReturn(10);
+ mockListUsersInfo(
+ mMockDevice,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, 0});
+ when(mMockDevice.switchUser(0)).thenReturn(true);
+
// act
mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
@@ -81,39 +156,18 @@
}
@Test
- public void testSetUpRunAsPrimary_ifNotInPrimary_switchToPrimary()
- throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
- // setup
- mockUsers(/* primaryUserId= */ 10, /* currentUserId= */ 11);
- mOptionSetter.setOptionValue("user-type", "primary");
-
- // act
- mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
-
- // assert it switches to primary in setUp
- verify(mMockDevice, times(1)).switchUser(10);
- }
-
- @Test
- public void testSetUpRunAsSystem_ifNotInSystem_switchToSystem()
- throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
- // setup
- mockUsers(/* primaryUserId= */ 10, /* currentUserId= */ 11);
- mOptionSetter.setOptionValue("user-type", "system");
-
- // act
- mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
-
- // assert it switches to primary in setUp
- verify(mMockDevice, times(1)).switchUser(USER_SYSTEM);
- }
-
- @Test
public void testTearDown_ifStartedInSecondary_switchesBackToSecondary()
throws DeviceNotAvailableException, TargetSetupError, ConfigurationException {
+ mOptionSetter.setOptionValue("user-type", "system");
+
// setup
- mockUsers(/* primaryUserId= */ 0, /* currentUserId= */ 10);
- mOptionSetter.setOptionValue("user-type", "primary");
+ when(mMockDevice.getCurrentUser()).thenReturn(10);
+ mockListUsersInfo(
+ mMockDevice,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, 0});
+ when(mMockDevice.switchUser(0)).thenReturn(true);
+ when(mMockDevice.switchUser(10)).thenReturn(true);
// first switches to primary
mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
@@ -122,13 +176,18 @@
// then switches back to secondary
mSwitchUserTargetPreparer.tearDown(mMockDevice, /* buildInfo= */ null, null);
verify(mMockDevice, times(1)).switchUser(10);
+
}
@Test
public void testSetUp_ifNoSwitchToSpecified_noUserSwitch()
throws DeviceNotAvailableException, TargetSetupError {
// setup
- mockUsers(/* primaryUserId= */ 0, /* currentUserId= */ 10);
+ when(mMockDevice.getCurrentUser()).thenReturn(10);
+ mockListUsersInfo(
+ mMockDevice,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {0, 0});
// act
mSwitchUserTargetPreparer.setUp(mMockDevice, /* buildInfo= */ null);
@@ -140,9 +199,14 @@
@Test
public void testSetUp_ifSwitchFails_throwsTargetSetupError()
throws DeviceNotAvailableException, ConfigurationException {
- // setup
- mockUsers(/* primaryUserId= */ 0, /* currentUserId= */ 11);
mOptionSetter.setOptionValue("user-type", "primary");
+
+ // setup
+ when(mMockDevice.getCurrentUser()).thenReturn(10);
+ mockListUsersInfo(
+ mMockDevice,
+ /* userIds= */ new Integer[] {0, 10},
+ /* flags= */ new Integer[] {UserInfo.FLAG_PRIMARY, 0});
when(mMockDevice.switchUser(0)).thenReturn(false);
// act
@@ -157,7 +221,24 @@
private void mockUsers(int primaryUserId, int currentUserId)
throws DeviceNotAvailableException {
when(mMockDevice.getCurrentUser()).thenReturn(currentUserId);
+
when(mMockDevice.getPrimaryUserId()).thenReturn(primaryUserId);
when(mMockDevice.switchUser(anyInt())).thenReturn(true);
}
+
+ private void mockListUsersInfo(ITestDevice device, Integer[] userIds, Integer[] flags)
+ throws DeviceNotAvailableException {
+ Map<Integer, UserInfo> result = new HashMap<>();
+ for (int i = 0; i < userIds.length; i++) {
+ int userId = userIds[i];
+ result.put(
+ userId,
+ new UserInfo(
+ /* userId= */ userId,
+ /* userName= */ "usr" + userId,
+ /* flag= */ flags[i],
+ /* isRunning= */ false));
+ }
+ when(device.getUserInfos()).thenReturn(result);
+ }
}
diff --git a/tests/src/com/android/tradefed/targetprep/app/NoApkTestSkipperTest.java b/tests/src/com/android/tradefed/targetprep/app/NoApkTestSkipperTest.java
index 3171040..d022139 100644
--- a/tests/src/com/android/tradefed/targetprep/app/NoApkTestSkipperTest.java
+++ b/tests/src/com/android/tradefed/targetprep/app/NoApkTestSkipperTest.java
@@ -20,7 +20,6 @@
import static org.junit.Assert.assertTrue;
import com.android.tradefed.build.AppBuildInfo;
-import com.android.tradefed.build.BuildInfo;
import com.android.tradefed.config.Configuration;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.device.ITestDevice;
@@ -52,11 +51,6 @@
}
@Test
- public void testNotAppBuild() throws Exception {
- mSkipper.setUp(mMockDevice, new BuildInfo());
- }
-
- @Test
public void testApksPresent() throws Exception {
mAppBuildInfo.addAppPackageFile(new File("fakepackage"), "v2");
mSkipper.setUp(mMockDevice, mAppBuildInfo);
diff --git a/tests/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparerTest.java b/tests/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparerTest.java
new file mode 100644
index 0000000..324d5e5
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/multi/DynamicSystemPreparerTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep.multi;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.matches;
+import static org.mockito.Mockito.doAnswer;
+
+import com.android.tradefed.build.DeviceBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.device.CollectingOutputReceiver;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.ZipUtil;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/** Unit tests for {@link com.android.tradefed.targetprep.multi.DynamicSystemPreparer}. */
+@RunWith(JUnit4.class)
+public class DynamicSystemPreparerTest {
+ // Input build info.
+ private static final String SYSTEM_IMAGE_NAME = "system.img";
+
+ private IInvocationContext mMockContext;
+ private IDeviceBuildInfo mSystemBuild;
+ private ITestDevice mMockDevice;
+ private File mSystemImageZip;
+ // The object under test.
+ private DynamicSystemPreparer mPreparer;
+
+ @Before
+ public void setUp() throws IOException {
+ mMockContext = Mockito.mock(InvocationContext.class);
+
+ mMockDevice = Mockito.mock(ITestDevice.class);
+ ITestDevice mockSystem = Mockito.mock(ITestDevice.class);
+ mSystemImageZip = createImageZip(SYSTEM_IMAGE_NAME);
+ mSystemBuild = createDeviceBuildInfo(mSystemImageZip);
+
+ Mockito.when(mMockContext.getDevice("device")).thenReturn(mMockDevice);
+ Mockito.when(mMockContext.getDevice("system")).thenReturn(mockSystem);
+ Mockito.when(mMockContext.getBuildInfo(mockSystem)).thenReturn(mSystemBuild);
+
+ mPreparer = new DynamicSystemPreparer();
+ }
+
+ @After
+ public void tearDown() {
+ if (mSystemBuild != null) {
+ mSystemBuild.cleanUp();
+ mSystemBuild = null;
+ }
+ FileUtil.deleteFile(mSystemImageZip);
+ }
+
+ private IDeviceBuildInfo createDeviceBuildInfo(File imageZip) {
+ IDeviceBuildInfo buildInfo = new DeviceBuildInfo();
+ buildInfo.setDeviceImageFile(imageZip, "");
+ return buildInfo;
+ }
+
+ private File createImageDir(String... fileNames) throws IOException {
+ File tempDir = FileUtil.createTempDir("createImageDir");
+ for (String fileName : fileNames) {
+ new File(tempDir, fileName).createNewFile();
+ }
+ return tempDir;
+ }
+
+ private File createImageZip(String... fileNames) throws IOException {
+ File tempDir = null;
+ try {
+ tempDir = createImageDir(fileNames);
+
+ ArrayList<File> tempFiles = new ArrayList<File>(fileNames.length);
+ for (String fileName : fileNames) {
+ tempFiles.add(new File(tempDir, fileName));
+ }
+
+ return ZipUtil.createZip(tempFiles);
+ } finally {
+ FileUtil.recursiveDelete(tempDir);
+ }
+ }
+
+ @Test
+ public void testSetUp()
+ throws TargetSetupError, BuildError, DeviceNotAvailableException, IOException {
+
+ File systemgz = new File("system.raw.gz");
+ Mockito.when(mMockDevice.pushFile(systemgz, "/sdcard/system.raw.gz"))
+ .thenReturn(Boolean.TRUE);
+ doAnswer(
+ new Answer<Object>() {
+ @Override
+ public Object answer(InvocationOnMock invocation) {
+ byte[] outputBytes = "running".getBytes();
+ ((CollectingOutputReceiver) invocation.getArguments()[1])
+ .addOutput(outputBytes, 0, outputBytes.length);
+ return null;
+ }
+ })
+ .when(mMockDevice)
+ .executeShellCommand(
+ matches("gsi_tool status"), any(CollectingOutputReceiver.class));
+ CommandResult res = new CommandResult();
+ res.setStdout("");
+ res.setStatus(CommandStatus.SUCCESS);
+ Mockito.when(mMockDevice.executeShellV2Command("gsi_tool enable")).thenReturn(res);
+ mPreparer.setUp(mMockContext);
+ }
+}
diff --git a/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java b/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java
new file mode 100644
index 0000000..3664ea4
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/multi/MixImageZipPreparerTest.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.targetprep.multi;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.build.DeviceBuildInfo;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.targetprep.multi.MixImageZipPreparer.InputStreamFactory;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.StreamUtil;
+import com.android.tradefed.util.ZipUtil;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+/** Unit tests for {@link MixImageZipPreparer}. */
+@RunWith(JUnit4.class)
+public class MixImageZipPreparerTest {
+ // Input build info.
+ private static final String VENDOR_IMAGE_NAME = "vendor.img";
+ private static final String SYSTEM_IMAGE_NAME = "system.img";
+ private static final String VBMETA_IMAGE_NAME = "vbmeta.img";
+ private static final String SYSTEM_BUILD_FLAVOR = "system_flavor";
+ private static final String SYSTEM_BUILD_ID = "123456";
+ private static final String DEVICE_LABEL = "device";
+ private static final String SYSTEM_LABEL = "system";
+ private static final String RESOURCE_LABEL = "resource";
+
+ // The strings written to temporary image files.
+ private static final String DEVICE_CONTENT = "device content";
+ private static final String SYSTEM_CONTENT = "system content";
+ private static final String RESOURCE_CONTENT = "resource content";
+
+ private IInvocationContext mMockContext;
+ private IDeviceBuildInfo mDeviceBuild;
+ private IDeviceBuildInfo mSystemBuild;
+ private IBuildInfo mResourceBuild;
+ private File mDeviceImageZip;
+ private File mSystemImageZip;
+ private File mResourceDir;
+
+ // The object under test.
+ private MixImageZipPreparer mPreparer;
+
+ private static class ByteArrayInputStreamFactory implements InputStreamFactory {
+ private final byte[] mData;
+ private List<InputStream> createdInputStreams;
+
+ private ByteArrayInputStreamFactory(String data) {
+ mData = data.getBytes();
+ createdInputStreams = new ArrayList<InputStream>();
+ }
+
+ @Override
+ public InputStream createInputStream() throws IOException {
+ InputStream stream = Mockito.spy(new ByteArrayInputStream(mData));
+ createdInputStreams.add(stream);
+ return stream;
+ }
+
+ @Override
+ public long getSize() {
+ return mData.length;
+ }
+
+ @Override
+ public long getCrc32() throws IOException {
+ // calculateCrc32 closes the stream.
+ return StreamUtil.calculateCrc32(createInputStream());
+ }
+ }
+
+ private void setUpPreparer() throws IOException {
+ mMockContext = Mockito.mock(InvocationContext.class);
+
+ ITestDevice mockDevice = Mockito.mock(ITestDevice.class);
+ ITestDevice mockSystem = Mockito.mock(ITestDevice.class);
+ mDeviceImageZip =
+ createImageZip(
+ DEVICE_CONTENT, VENDOR_IMAGE_NAME, SYSTEM_IMAGE_NAME, VBMETA_IMAGE_NAME);
+ mSystemImageZip = createImageZip(SYSTEM_CONTENT, SYSTEM_IMAGE_NAME);
+ mDeviceBuild = createDeviceBuildInfo("device_flavor", "device_build_id", mDeviceImageZip);
+ mSystemBuild = createDeviceBuildInfo(SYSTEM_BUILD_FLAVOR, SYSTEM_BUILD_ID, mSystemImageZip);
+
+ Mockito.when(mMockContext.getDevice(DEVICE_LABEL)).thenReturn(mockDevice);
+ Mockito.when(mMockContext.getBuildInfo(mockDevice)).thenReturn(mDeviceBuild);
+ Mockito.when(mMockContext.getDevice(SYSTEM_LABEL)).thenReturn(mockSystem);
+ Mockito.when(mMockContext.getBuildInfo(mockSystem)).thenReturn(mSystemBuild);
+
+ mPreparer = new MixImageZipPreparer();
+ mPreparer.addSystemFileName(SYSTEM_IMAGE_NAME);
+ }
+
+ private void setUpResource() throws IOException {
+ ITestDevice mockResource = Mockito.mock(ITestDevice.class);
+ mResourceDir = createImageDir(RESOURCE_CONTENT, VBMETA_IMAGE_NAME);
+ mResourceBuild = createBuildInfo(mResourceDir);
+
+ Mockito.when(mMockContext.getDevice(RESOURCE_LABEL)).thenReturn(mockResource);
+ Mockito.when(mMockContext.getBuildInfo(mockResource)).thenReturn(mResourceBuild);
+
+ mPreparer.addResourceFileName(VBMETA_IMAGE_NAME);
+ }
+
+ @After
+ public void tearDown() {
+ if (mDeviceBuild != null) {
+ mDeviceBuild.cleanUp();
+ mDeviceBuild = null;
+ }
+ if (mSystemBuild != null) {
+ mSystemBuild.cleanUp();
+ mSystemBuild = null;
+ }
+ if (mResourceBuild != null) {
+ mResourceBuild.cleanUp();
+ mResourceBuild = null;
+ }
+ if (mDeviceImageZip != null) {
+ mDeviceImageZip.delete();
+ mDeviceImageZip = null;
+ }
+ if (mSystemImageZip != null) {
+ mSystemImageZip.delete();
+ mSystemImageZip = null;
+ }
+ if (mResourceDir != null) {
+ FileUtil.recursiveDelete(mResourceDir);
+ mResourceDir = null;
+ }
+ }
+
+ private IDeviceBuildInfo createDeviceBuildInfo(
+ String buildFlavor, String buildId, File imageZip) {
+ IDeviceBuildInfo buildInfo = new DeviceBuildInfo();
+ buildInfo.setBuildFlavor(buildFlavor);
+ buildInfo.setBuildId(buildId);
+ buildInfo.setDeviceImageFile(imageZip, buildId);
+ return buildInfo;
+ }
+
+ private IBuildInfo createBuildInfo(File rootDir) {
+ BuildInfo buildInfo = new BuildInfo();
+ for (File file : rootDir.listFiles()) {
+ buildInfo.setFile(file.getName(), file, "0");
+ }
+ return buildInfo;
+ }
+
+ private File createImageDir(String content, String... fileNames) throws IOException {
+ File tempDir = FileUtil.createTempDir("createImageDir");
+ for (String fileName : fileNames) {
+ try (FileWriter writer = new FileWriter(new File(tempDir, fileName))) {
+ writer.write(content);
+ }
+ }
+ return tempDir;
+ }
+
+ private File createImageZip(String content, String... fileNames) throws IOException {
+ // = new ArrayList<File>(fileNames.length);
+ File tempDir = null;
+ try {
+ tempDir = createImageDir(content, fileNames);
+
+ ArrayList<File> tempFiles = new ArrayList<File>(fileNames.length);
+ for (String fileName : fileNames) {
+ tempFiles.add(new File(tempDir, fileName));
+ }
+
+ return ZipUtil.createZip(tempFiles);
+ } finally {
+ FileUtil.recursiveDelete(tempDir);
+ }
+ }
+
+ private void verifyImage(String content, File dir, String fileName)
+ throws FileNotFoundException, IOException {
+ try (FileReader reader = new FileReader(new File(dir, fileName))) {
+ char[] buffer = new char[content.length()];
+ reader.read(buffer);
+ Assert.assertEquals(content, new String(buffer));
+ Assert.assertTrue("Image contains extra content.", reader.read() < 0);
+ }
+ }
+
+ private void verifyImageZip(File imageZip) throws FileNotFoundException, IOException {
+ File mixedImageDir = ZipUtil.extractZipToTemp(imageZip, "verifyImageZip");
+ try {
+ verifyImage(DEVICE_CONTENT, mixedImageDir, VENDOR_IMAGE_NAME);
+ verifyImage(SYSTEM_CONTENT, mixedImageDir, SYSTEM_IMAGE_NAME);
+ if (mResourceBuild != null) {
+ verifyImage(RESOURCE_CONTENT, mixedImageDir, VBMETA_IMAGE_NAME);
+ }
+ } finally {
+ FileUtil.recursiveDelete(mixedImageDir);
+ }
+ }
+
+ private void runPreparerTest()
+ throws TargetSetupError, BuildError, DeviceNotAvailableException, ZipException,
+ IOException {
+ mPreparer.setUp(mMockContext);
+
+ ArgumentCaptor<IBuildInfo> argument = ArgumentCaptor.forClass(IBuildInfo.class);
+ Mockito.verify(mMockContext)
+ .addDeviceBuildInfo(Mockito.eq(DEVICE_LABEL), argument.capture());
+ IDeviceBuildInfo addedBuildInfo = ((IDeviceBuildInfo) argument.getValue());
+ try {
+ Assert.assertFalse("Device build is not cleaned up.", mDeviceImageZip.exists());
+ mDeviceImageZip = null;
+ mDeviceBuild = null;
+
+ Assert.assertEquals(SYSTEM_BUILD_FLAVOR, addedBuildInfo.getBuildFlavor());
+ Assert.assertEquals(SYSTEM_BUILD_ID, addedBuildInfo.getDeviceBuildId());
+ verifyImageZip(addedBuildInfo.getDeviceImageFile());
+ } finally {
+ addedBuildInfo.cleanUp();
+ }
+ }
+
+ /**
+ * Test that the mixed {@link IDeviceBuildInfo} contains the resource file and works with
+ * non-default compression level.
+ */
+ @Test
+ public void testSetUpWithResource()
+ throws TargetSetupError, BuildError, DeviceNotAvailableException, IOException {
+ setUpPreparer();
+ setUpResource();
+ mPreparer.setCompressionLevel(0);
+ runPreparerTest();
+ }
+
+ /**
+ * Test that the mixed {@link IDeviceBuildInfo} contains the system build's image, build flavor,
+ * and build id.
+ */
+ @Test
+ public void testSetUpWithSystem()
+ throws TargetSetupError, BuildError, DeviceNotAvailableException, IOException {
+ setUpPreparer();
+ runPreparerTest();
+ }
+
+ private void runCreateZipTest(int compressionLevel) throws IOException {
+ Map<String, ByteArrayInputStreamFactory> data =
+ new HashMap<String, ByteArrayInputStreamFactory>();
+ data.put("entry1", new ByteArrayInputStreamFactory("abcabcabcabcabcabc"));
+ data.put("entry2", new ByteArrayInputStreamFactory("01230123012301230123"));
+
+ File file = null;
+ ZipFile zipFile = null;
+ try {
+ file = MixImageZipPreparer.createZip(data, compressionLevel);
+ zipFile = new ZipFile(file);
+
+ Assert.assertEquals(data.size(), zipFile.stream().count());
+ for (Map.Entry<String, ByteArrayInputStreamFactory> entry : data.entrySet()) {
+ ByteArrayInputStreamFactory expected = entry.getValue();
+ ZipEntry actual = zipFile.getEntry(entry.getKey());
+ Assert.assertEquals(expected.getSize(), actual.getSize());
+ Assert.assertEquals(expected.getCrc32(), actual.getCrc());
+ if (compressionLevel == Deflater.NO_COMPRESSION) {
+ Assert.assertEquals(expected.getSize(), actual.getCompressedSize());
+ } else {
+ Assert.assertTrue(expected.getSize() > actual.getCompressedSize());
+ }
+
+ for (InputStream stream : expected.createdInputStreams) {
+ Mockito.verify(stream).close();
+ }
+ }
+ } finally {
+ ZipUtil.closeZip(zipFile);
+ FileUtil.deleteFile(file);
+ }
+ }
+
+ /** Verify createZip with default compression level. */
+ @Test
+ public void testCreateZip() throws IOException {
+ runCreateZipTest(Deflater.DEFAULT_COMPRESSION);
+ }
+
+ /** Verify createZip with no compression. */
+ @Test
+ public void testCreateZipWithNoCompression() throws IOException {
+ runCreateZipTest(Deflater.NO_COMPRESSION);
+ }
+}
diff --git a/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java b/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java
index 82ef7c9..3e691b8 100644
--- a/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/DeviceJUnit4ClassRunnerTest.java
@@ -68,7 +68,7 @@
new DynamicRemoteFileResolver() {
@Override
protected IRemoteFileResolver getResolver(String protocol) {
- if (protocol.equals(GcsRemoteFileResolver.PROTOCOL)) {
+ if (GcsRemoteFileResolver.PROTOCOL.equals(protocol)) {
IRemoteFileResolver mockResolver =
Mockito.mock(IRemoteFileResolver.class);
try {
@@ -76,6 +76,7 @@
.when(mockResolver)
.resolveRemoteFiles(
Mockito.eq(FAKE_REMOTE_FILE_PATH),
+ Mockito.any(),
Mockito.any());
return mockResolver;
} catch (ConfigurationException e) {
diff --git a/tests/src/com/android/tradefed/testtype/GTestBaseTest.java b/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
index fa34c71..27a816f 100644
--- a/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestBaseTest.java
@@ -195,7 +195,7 @@
public void testCoverage_addsCodeCoverageListener() throws ConfigurationException {
GTestBase gTestBase = new GTestBaseImpl();
mSetter = new OptionSetter(gTestBase);
- mSetter.setOptionValue("native-coverage", "true");
+ mSetter.setOptionValue("coverage", "true");
ITestInvocationListener listener =
gTestBase.addNativeCoverageListenerIfEnabled(mMockTestDevice, mMockListener);
@@ -208,7 +208,7 @@
public void testNoCoverage_doesNotAddCodeCoverageListener() throws ConfigurationException {
GTestBase gTestBase = new GTestBaseImpl();
mSetter = new OptionSetter(gTestBase);
- mSetter.setOptionValue("native-coverage", "false");
+ mSetter.setOptionValue("coverage", "false");
ITestInvocationListener listener =
gTestBase.addNativeCoverageListenerIfEnabled(mMockTestDevice, mMockListener);
diff --git a/tests/src/com/android/tradefed/testtype/GTestTest.java b/tests/src/com/android/tradefed/testtype/GTestTest.java
index f47190f..56a2b6f 100644
--- a/tests/src/com/android/tradefed/testtype/GTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestTest.java
@@ -36,6 +36,8 @@
import org.junit.runners.JUnit4;
import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -140,6 +142,48 @@
String[] files = new String[] {"test1", "test2"};
EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
+ mMockITestDevice.executeShellCommand(
+ EasyMock.contains(test1),
+ EasyMock.same(mMockReceiver),
+ EasyMock.anyLong(),
+ (TimeUnit) EasyMock.anyObject(),
+ EasyMock.anyInt());
+ mMockITestDevice.executeShellCommand(
+ EasyMock.contains(test2),
+ EasyMock.same(mMockReceiver),
+ EasyMock.anyLong(),
+ (TimeUnit) EasyMock.anyObject(),
+ EasyMock.anyInt());
+
+ replayMocks();
+
+ mGTest.run(mMockInvocationListener);
+ verifyMocks();
+ }
+
+ /** Test the run method without clearing coverage before running the tests. */
+ @Test
+ public void testRunNoCoverageClear() throws Exception {
+ mSetter.setOptionValue("coverage-clear-before-test", "false");
+
+ final String nativeTestPath = GTest.DEFAULT_NATIVETEST_PATH;
+ final String test1 = "test1";
+ final String test2 = "test2";
+ final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
+ final String testPath2 = String.format("%s/%s", nativeTestPath, test2);
+
+ MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1, test2);
+ EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+ // report the file as executable
+ EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
+ // report the file as executable
+ EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
+
+ String[] files = new String[] {"test1", "test2"};
+ EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
mMockITestDevice.executeShellCommand(EasyMock.contains(test1),
EasyMock.same(mMockReceiver), EasyMock.anyLong(),
(TimeUnit)EasyMock.anyObject(), EasyMock.anyInt());
@@ -426,6 +470,121 @@
verifyMocks();
}
+ /** Test cross-process coverage dump for all native processes */
+ @Test
+ public void testNativeCoverageAllProcesses() throws Exception {
+ mSetter.setOptionValue("coverage", "true");
+ mSetter.setOptionValue("coverage-flush", "true");
+
+ final String nativeTestPath = GTest.DEFAULT_NATIVETEST_PATH;
+ final String test1 = "test1";
+ final String test2 = "test2";
+ final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
+ final String testPath2 = String.format("%s/%s", nativeTestPath, test2);
+
+ MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1, test2);
+ EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+ EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
+ EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+ EasyMock.expect(mMockITestDevice.executeShellCommand("rm -rf /data/misc/trace/*"))
+ .andReturn("");
+ EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+ // report the file as executable
+ EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
+ // report the file as executable
+ EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
+
+ String[] files = new String[] {"test1", "test2"};
+ EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
+ mMockITestDevice.executeShellCommand(
+ EasyMock.contains(test1),
+ EasyMock.same(mMockReceiver),
+ EasyMock.anyLong(),
+ (TimeUnit) EasyMock.anyObject(),
+ EasyMock.anyInt());
+ mMockITestDevice.executeShellCommand(
+ EasyMock.contains(test2),
+ EasyMock.same(mMockReceiver),
+ EasyMock.anyLong(),
+ (TimeUnit) EasyMock.anyObject(),
+ EasyMock.anyInt());
+
+ EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+ EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 -1")).andReturn("");
+
+ replayMocks();
+
+ mGTest.run(mMockInvocationListener);
+ verifyMocks();
+ }
+
+ /** Test cross-process coverage dump for specific processes */
+ @Test
+ public void testNativeCoverageSpecificProcesses() throws Exception {
+ mSetter.setOptionValue("coverage", "true");
+ mSetter.setOptionValue("coverage-flush", "true");
+
+ final List<String> processNames = new ArrayList<>();
+ processNames.add("init");
+ processNames.add("surfaceflinger");
+
+ mGTest.setCoverageProcesses(processNames);
+
+ final String nativeTestPath = GTest.DEFAULT_NATIVETEST_PATH;
+ final String test1 = "test1";
+ final String test2 = "test2";
+ final String testPath1 = String.format("%s/%s", nativeTestPath, test1);
+ final String testPath2 = String.format("%s/%s", nativeTestPath, test2);
+
+ MockFileUtil.setMockDirContents(mMockITestDevice, nativeTestPath, test1, test2);
+ // Get the pids to flush coverage data.
+ EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+ EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(0))).andReturn("1");
+ EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(1))).andReturn("1000");
+ EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 1 1000")).andReturn("");
+
+ // Clear the coverage data.
+ EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+ EasyMock.expect(mMockITestDevice.executeShellCommand("rm -rf /data/misc/trace/*"))
+ .andReturn("");
+ EasyMock.expect(mMockITestDevice.doesFileExist(nativeTestPath)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(nativeTestPath)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(testPath1)).andReturn(false);
+ // report the file as executable
+ EasyMock.expect(mMockITestDevice.isExecutable(testPath1)).andReturn(true);
+ EasyMock.expect(mMockITestDevice.isDirectory(testPath2)).andReturn(false);
+ // report the file as executable
+ EasyMock.expect(mMockITestDevice.isExecutable(testPath2)).andReturn(true);
+
+ String[] files = new String[] {"test1", "test2"};
+ EasyMock.expect(mMockITestDevice.getChildren(nativeTestPath)).andReturn(files);
+ mMockITestDevice.executeShellCommand(
+ EasyMock.contains(test1),
+ EasyMock.same(mMockReceiver),
+ EasyMock.anyLong(),
+ (TimeUnit) EasyMock.anyObject(),
+ EasyMock.anyInt());
+ mMockITestDevice.executeShellCommand(
+ EasyMock.contains(test2),
+ EasyMock.same(mMockReceiver),
+ EasyMock.anyLong(),
+ (TimeUnit) EasyMock.anyObject(),
+ EasyMock.anyInt());
+
+ EasyMock.expect(mMockITestDevice.isAdbRoot()).andReturn(true);
+ EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(0))).andReturn("1");
+ EasyMock.expect(mMockITestDevice.getProcessPid(processNames.get(1))).andReturn("1000");
+ EasyMock.expect(mMockITestDevice.executeShellCommand("kill -37 1 1000")).andReturn("");
+
+ replayMocks();
+
+ mGTest.run(mMockInvocationListener);
+ verifyMocks();
+ }
+
@Test
public void testGetFileName() {
String expected = "bar";
diff --git a/tests/src/com/android/tradefed/testtype/HostTestTest.java b/tests/src/com/android/tradefed/testtype/HostTestTest.java
index a5d8387..0b63d72 100644
--- a/tests/src/com/android/tradefed/testtype/HostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/HostTestTest.java
@@ -504,7 +504,7 @@
new DynamicRemoteFileResolver() {
@Override
protected IRemoteFileResolver getResolver(String protocol) {
- if (protocol.equals(GcsRemoteFileResolver.PROTOCOL)) {
+ if (GcsRemoteFileResolver.PROTOCOL.equals(protocol)) {
return mRemoteFileResolver;
}
return null;
@@ -672,7 +672,8 @@
public void testRun_junit3TestSuite_dynamicOptions() throws Exception {
doReturn(new File("/downloaded/somewhere"))
.when(mMockResolver)
- .resolveRemoteFiles(Mockito.eq(FAKE_REMOTE_FILE_PATH), Mockito.any());
+ .resolveRemoteFiles(
+ Mockito.eq(FAKE_REMOTE_FILE_PATH), Mockito.any(), Mockito.any());
mHostTest.setClassName(DynamicTestCase.class.getName());
TestDescription test1 = new TestDescription(DynamicTestCase.class.getName(), "testPass");
mListener.testRunStarted((String) EasyMock.anyObject(), EasyMock.eq(1));
diff --git a/tests/src/com/android/tradefed/testtype/InstalledInstrumentationsTestTest.java b/tests/src/com/android/tradefed/testtype/InstalledInstrumentationsTestTest.java
index 9f955cb..cd318f0 100644
--- a/tests/src/com/android/tradefed/testtype/InstalledInstrumentationsTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstalledInstrumentationsTestTest.java
@@ -23,10 +23,8 @@
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.metric.BaseDeviceMetricCollector;
import com.android.tradefed.device.metric.IMetricCollector;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
-import org.easymock.Capture;
import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.junit.Before;
@@ -36,7 +34,6 @@
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
/** Unit tests for {@link InstalledInstrumentationsTest}. */
@@ -71,12 +68,8 @@
injectShellResponse(String.format(INSTR_OUTPUT_FORMAT, TEST_PKG, TEST_RUNNER,
TEST_COVERAGE_TARGET), 1);
- mMockListener.testRunStarted(TEST_PKG, 0);
- Capture<HashMap<String, Metric>> captureMetrics = new Capture<>();
- mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.capture(captureMetrics));
ArgsOptionParser p = new ArgsOptionParser(mInstalledInstrTest);
p.parse("--size", "small", "--force-abi", ABI);
- mInstalledInstrTest.setSendCoverage(true);
EasyMock.replay(mMockTestDevice, mMockListener);
mInstalledInstrTest.run(mMockListener);
assertEquals(1, mMockInstrumentationTests.size());
@@ -84,13 +77,6 @@
assertEquals(mMockListener, mockInstrumentationTest.getListener());
assertEquals(TEST_PKG, mockInstrumentationTest.getPackageName());
assertEquals(TEST_RUNNER, mockInstrumentationTest.getRunnerName());
- assertEquals(
- TEST_COVERAGE_TARGET,
- captureMetrics
- .getValue()
- .get(InstalledInstrumentationsTest.COVERAGE_TARGET_KEY)
- .getMeasurements()
- .getSingleString());
assertEquals("small", mockInstrumentationTest.getTestSize());
assertEquals(ABI, mockInstrumentationTest.getForceAbi());
@@ -156,12 +142,8 @@
injectShellResponse(
String.format(INSTR_OUTPUT_FORMAT, TEST_PKG, TEST_RUNNER, TEST_COVERAGE_TARGET), 1);
- mMockListener.testRunStarted(TEST_PKG, 0);
- Capture<HashMap<String, Metric>> captureMetrics = new Capture<>();
- mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.capture(captureMetrics));
ArgsOptionParser p = new ArgsOptionParser(mInstalledInstrTest);
p.parse("--size", "small", "--force-abi", ABI);
- mInstalledInstrTest.setSendCoverage(true);
List<IMetricCollector> collectors = new ArrayList<>();
collectors.add(new BaseDeviceMetricCollector());
mInstalledInstrTest.setMetricCollectors(collectors);
@@ -172,13 +154,6 @@
assertEquals(mMockListener, mockInstrumentationTest.getListener());
assertEquals(TEST_PKG, mockInstrumentationTest.getPackageName());
assertEquals(TEST_RUNNER, mockInstrumentationTest.getRunnerName());
- assertEquals(
- TEST_COVERAGE_TARGET,
- captureMetrics
- .getValue()
- .get(InstalledInstrumentationsTest.COVERAGE_TARGET_KEY)
- .getMeasurements()
- .getSingleString());
assertEquals("small", mockInstrumentationTest.getTestSize());
assertEquals(ABI, mockInstrumentationTest.getForceAbi());
assertEquals(1, mockInstrumentationTest.getCollectors().size());
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
index f00cab1..e91fb88 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationFileTestTest.java
@@ -307,6 +307,7 @@
}
};
setRunTestExpectations(secdondSerialRunAnswer);
+ mMockTestDevice.waitForDeviceAvailable();
mInstrumentationFileTest = new InstrumentationFileTest(mMockITest, testsList, true, -1) {
@Override
diff --git a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
index c5d0f27..aaef451 100644
--- a/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/InstrumentationTestTest.java
@@ -26,6 +26,7 @@
import static org.mockito.ArgumentMatchers.anyCollectionOf;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
@@ -42,6 +43,7 @@
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.metric.IMetricCollector;
+import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ByteArrayInputStreamSource;
import com.android.tradefed.result.CollectingTestListener;
@@ -530,6 +532,8 @@
.when(mMockTestDevice)
.runInstrumentationTests(
any(IRemoteAndroidTestRunner.class), any(ITestLifeCycleReceiver.class));
+ doReturn(true).when(mMockTestDevice).enableAdbRoot();
+ doReturn("").when(mMockTestDevice).executeShellCommand(anyString());
mInstrumentationTest.run(mMockListener);
@@ -922,6 +926,9 @@
ITestInvocationListener listener =
mInstrumentationTest.addJavaCoverageListenerIfEnabled(mMockListener);
assertThat(listener).isInstanceOf(JavaCodeCoverageListener.class);
+
+ listener = mInstrumentationTest.addNativeCoverageListenerIfEnabled(mMockListener);
+ assertThat(listener).isInstanceOf(NativeCodeCoverageListener.class);
}
@Test
@@ -931,6 +938,9 @@
ITestInvocationListener listener =
mInstrumentationTest.addJavaCoverageListenerIfEnabled(mMockListener);
assertThat(listener).isSameAs(mMockListener);
+
+ listener = mInstrumentationTest.addNativeCoverageListenerIfEnabled(mMockListener);
+ assertThat(listener).isSameAs(mMockListener);
}
/** Test normal run scenario when {@link IMetricCollector} are specified. */
@@ -955,10 +965,13 @@
List<IMetricCollector> collectors = new ArrayList<>();
CalledMetricCollector calledCollector = new CalledMetricCollector();
+ calledCollector.mName = "called";
CalledMetricCollector notCalledCollector = new CalledMetricCollector();
notCalledCollector.setDisable(true);
+ notCalledCollector.mName = "not-called";
collectors.add(notCalledCollector);
collectors.add(calledCollector);
+ mInstrumentationTest.setInvocationContext(new InvocationContext());
mInstrumentationTest.setMetricCollectors(collectors);
mInstrumentationTest.run(mMockListener);
@@ -968,16 +981,26 @@
inOrder.verify(mInstrumentationTest).setRunnerArgs(runner.capture());
inOrder.verify(mMockTestDevice, times(2))
.runInstrumentationTests(eq(runner.getValue()), any(ITestLifeCycleReceiver.class));
-
inOrder.verify(mMockListener).testRunStarted(TEST_PACKAGE_VALUE, 2);
inOrder.verify(mMockListener).testStarted(eq(TEST1), anyLong());
- inOrder.verify(mMockListener).testEnded(eq(TEST1), anyLong(), eq(EMPTY_STRING_MAP));
+ ArgumentCaptor<HashMap<String, Metric>> testCapture1 =
+ ArgumentCaptor.forClass(HashMap.class);
+ inOrder.verify(mMockListener).testEnded(eq(TEST1), anyLong(), testCapture1.capture());
+ HashMap<String, Metric> test1Metric = testCapture1.getValue();
+ assertTrue(test1Metric.containsKey("called"));
+ assertFalse(test1Metric.containsKey("not-called"));
inOrder.verify(mMockListener).testStarted(eq(TEST2), anyLong());
- inOrder.verify(mMockListener).testEnded(eq(TEST2), anyLong(), eq(EMPTY_STRING_MAP));
- inOrder.verify(mMockListener).testRunEnded(1, EMPTY_STRING_MAP);
-
- assertTrue(calledCollector.wasCalled);
- assertFalse(notCalledCollector.wasCalled);
+ ArgumentCaptor<HashMap<String, Metric>> testCapture2 =
+ ArgumentCaptor.forClass(HashMap.class);
+ inOrder.verify(mMockListener).testEnded(eq(TEST2), anyLong(), testCapture2.capture());
+ HashMap<String, Metric> test2Metric = testCapture2.getValue();
+ assertTrue(test2Metric.containsKey("called"));
+ assertFalse(test2Metric.containsKey("not-called"));
+ ArgumentCaptor<HashMap<String, Metric>> runCapture = ArgumentCaptor.forClass(HashMap.class);
+ inOrder.verify(mMockListener).testRunEnded(anyLong(), runCapture.capture());
+ HashMap<String, Metric> runMetric = runCapture.getValue();
+ assertTrue(runMetric.containsKey("called"));
+ assertFalse(runMetric.containsKey("not-called"));
}
private static class FakeTestRunner extends RemoteAndroidTestRunner {
diff --git a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java b/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
index dd65aca..98d88eb 100644
--- a/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
+++ b/tests/src/com/android/tradefed/testtype/NativeCodeCoverageListenerTest.java
@@ -59,6 +59,7 @@
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
+import java.util.zip.ZipFile;
/** Unit tests for {@link NativeCodeCoverageListener}. */
@RunWith(JUnit4.class)
@@ -90,10 +91,12 @@
@Test
public void test_logsCoverageZip() throws DeviceNotAvailableException, IOException {
// Setup mocks to write the coverage measurement to the file.
+ doReturn(true).when(mMockDevice).enableAdbRoot();
doReturn(
new StringJoiner("\n")
- .add("/data/misc/trace/path/to/coverage.gcda")
- .add("/data/misc/trace/path/to/.hidden/coverage2.gcda")
+ .add("/data/misc/trace/proc/self/cwd/out/path/to/coverage.gcda")
+ .add(
+ "/data/misc/trace/proc/self/cwd/out/path/to/.hidden/coverage2.gcda")
.toString())
.when(mMockDevice)
.executeShellCommand(anyString());
@@ -135,9 +138,32 @@
}
@Test
+ public void testNoCoverageFiles_logsEmptyZip() throws DeviceNotAvailableException, IOException {
+ doReturn(true).when(mMockDevice).enableAdbRoot();
+ doReturn("").when(mMockDevice).executeShellCommand(anyString());
+
+ // Simulate a test run.
+ mCodeCoverageListener.testRunStarted(RUN_NAME, TEST_COUNT);
+ Map<String, String> metric = new HashMap<>();
+ mCodeCoverageListener.testRunEnded(ELAPSED_TIME, TfMetricProtoUtil.upgradeConvert(metric));
+
+ // Verify testLog(..) was called with an empty zip.
+ List<ByteString> logs = mFakeListener.getLogs();
+ assertThat(logs).hasSize(1);
+ File outputZip = folder.newFile("empty_coverage.zip");
+ try (OutputStream out = new FileOutputStream(outputZip)) {
+ logs.get(0).writeTo(out);
+ }
+
+ ZipFile loggedZip = new ZipFile(outputZip);
+ assertThat(loggedZip.size()).isEqualTo(0);
+ }
+
+ @Test
public void testFailure_unableToPullFile() throws DeviceNotAvailableException {
// Setup mocks.
- doReturn("/data/misc/trace/some/path/to/coverage.gcda\n")
+ doReturn(true).when(mMockDevice).enableAdbRoot();
+ doReturn("/data/misc/trace/proc/self/cwd/out/some/path/to/coverage.gcda\n")
.when(mMockDevice)
.executeShellCommand(anyString());
doReturn(false).when(mMockDevice).pullFile(anyString(), any());
diff --git a/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java b/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java
deleted file mode 100644
index 7424240..0000000
--- a/tests/src/com/android/tradefed/testtype/VersionedTfLauncherTest.java
+++ /dev/null
@@ -1,381 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import com.android.ddmlib.IDevice;
-import com.android.tradefed.build.IBuildInfo;
-import com.android.tradefed.build.IFolderBuildInfo;
-import com.android.tradefed.command.CommandOptions;
-import com.android.tradefed.config.GlobalConfiguration;
-import com.android.tradefed.config.IConfiguration;
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.device.NullDevice;
-import com.android.tradefed.device.StubDevice;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.FileInputStreamSource;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.LogDataType;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.util.CommandResult;
-import com.android.tradefed.util.CommandStatus;
-import com.android.tradefed.util.IRunUtil;
-import com.android.tradefed.util.IRunUtil.EnvPriority;
-
-import org.easymock.EasyMock;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-
-/** Unit tests for {@link VersionedTfLauncher}. */
-@RunWith(JUnit4.class)
-public class VersionedTfLauncherTest {
-
- private static final String FAKE_SERIAL = "FAKE_SERIAL";
- private static final String CONFIG_NAME = "FAKE_CONFIG";
- private static final String TF_COMMAND_LINE_TEMPLATE = "--template:map";
- private static final String TF_COMMAND_LINE_TEST = "test=tf/fake";
- // Test option value with empty spaces should be parsed correctly.
- private static final String TF_COMMAND_LINE_OPTION = "--option";
- private static final String TF_COMMAND_LINE_OPTION_VALUE = "value1 value2";
- private static final String TF_COMMAND_LINE_OPTION_VALUE_QUOTED =
- ("\\\"" + TF_COMMAND_LINE_OPTION_VALUE + "\\\"");
- private static final String TF_COMMAND_LINE =
- (TF_COMMAND_LINE_TEMPLATE + " " + TF_COMMAND_LINE_TEST + " " + TF_COMMAND_LINE_OPTION +
- " " + TF_COMMAND_LINE_OPTION_VALUE_QUOTED);
- private static final String ADDITIONAL_TEST_ZIP = "/tmp/tests.zip";
-
- private VersionedTfLauncher mVersionedTfLauncher;
- private ITestInvocationListener mMockListener;
- private IRunUtil mMockRunUtil;
- private ITestDevice mMockTestDevice;
- private IDevice mMockIDevice;
- private IFolderBuildInfo mMockBuildInfo;
- private IConfiguration mMockConfig;
-
- @Before
- public void setUp() throws Exception {
- mMockListener = EasyMock.createMock(ITestInvocationListener.class);
- mMockRunUtil = EasyMock.createMock(IRunUtil.class);
- mMockBuildInfo = EasyMock.createMock(IFolderBuildInfo.class);
- mMockTestDevice = EasyMock.createMock(ITestDevice.class);
- mMockConfig = EasyMock.createMock(IConfiguration.class);
-
- mVersionedTfLauncher = new VersionedTfLauncher();
- mVersionedTfLauncher.setRunUtil(mMockRunUtil);
- mVersionedTfLauncher.setBuild(mMockBuildInfo);
- mVersionedTfLauncher.setEventStreaming(false);
- mVersionedTfLauncher.setConfiguration(mMockConfig);
-
- OptionSetter setter = new OptionSetter(mVersionedTfLauncher);
- setter.setOptionValue("config-name", CONFIG_NAME);
- setter.setOptionValue("tf-command-line", TF_COMMAND_LINE);
- setter.setOptionValue("inject-invocation-data", "true");
-
- try {
- GlobalConfiguration.createGlobalConfiguration(new String[] {});
- } catch (IllegalStateException e) {
- // ignore re-init.
- }
- }
-
- /**
- * Test {@link VersionedTfLauncher#run(ITestInvocationListener)} for test with a single device
- */
- @Test
- public void testRun_singleDevice() {
- mMockIDevice = EasyMock.createMock(IDevice.class);
-
- CommandResult cr = new CommandResult(CommandStatus.SUCCESS);
- mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
- mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
- mMockRunUtil.unsetEnvVariable(SubprocessTfLauncher.ANDROID_SERIAL_VAR);
- mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
- mMockRunUtil.setEnvVariable(
- EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE),
- (String) EasyMock.anyObject());
-
- EasyMock.expect(
- mMockRunUtil.runTimedCmd(
- EasyMock.anyLong(),
- (FileOutputStream) EasyMock.anyObject(),
- (FileOutputStream) EasyMock.anyObject(),
- EasyMock.eq("java"),
- (String) EasyMock.anyObject(),
- EasyMock.eq("--add-opens=java.base/java.nio=ALL-UNNAMED"),
- EasyMock.eq("-cp"),
- (String) EasyMock.anyObject(),
- EasyMock.eq("com.android.tradefed.command.CommandRunner"),
- EasyMock.eq(CONFIG_NAME),
- EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
- EasyMock.eq(TF_COMMAND_LINE_TEST),
- EasyMock.eq(TF_COMMAND_LINE_OPTION),
- EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
- EasyMock.eq("--serial"),
- EasyMock.eq(FAKE_SERIAL),
- EasyMock.eq("--additional-tests-zip"),
- EasyMock.eq(ADDITIONAL_TEST_ZIP),
- EasyMock.eq("--" + CommandOptions.INVOCATION_DATA),
- EasyMock.eq(SubprocessTfLauncher.SUBPROCESS_TAG_NAME),
- EasyMock.eq("true"),
- EasyMock.eq("--subprocess-report-file"),
- (String) EasyMock.anyObject()))
- .andReturn(cr);
- Map<ITestDevice, IBuildInfo> deviceInfos = new HashMap<ITestDevice, IBuildInfo>();
- deviceInfos.put(mMockTestDevice, null);
- mVersionedTfLauncher.setDeviceInfos(deviceInfos);
- EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
- EasyMock.expect(mMockBuildInfo.getBuildId()).andReturn("FAKEID").times(2);
- EasyMock.expect(mMockBuildInfo.getFile("general-tests.zip"))
- .andReturn(new File(ADDITIONAL_TEST_ZIP));
- mMockBuildInfo.addBuildAttribute(SubprocessTfLauncher.PARENT_PROC_TAG_NAME, "true");
- EasyMock.expect(mMockTestDevice.getIDevice()).andReturn(mMockIDevice).times(2);
- EasyMock.expect(mMockTestDevice.getSerialNumber()).andReturn(FAKE_SERIAL).times(1);
- mMockListener.testLog((String)EasyMock.anyObject(), (LogDataType)EasyMock.anyObject(),
- (FileInputStreamSource)EasyMock.anyObject());
- EasyMock.expectLastCall().times(3);
- mMockListener.testRunStarted("StdErr", 1);
- mMockListener.testStarted((TestDescription) EasyMock.anyObject());
- mMockListener.testEnded(
- (TestDescription) EasyMock.anyObject(), EasyMock.eq(new HashMap<String, Metric>()));
- mMockListener.testRunEnded(0, new HashMap<String, Metric>());
-
- EasyMock.expect(mMockConfig.getCommandOptions()).andReturn(new CommandOptions());
- EasyMock.replay(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
- mVersionedTfLauncher.run(mMockListener);
- EasyMock.verify(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
- }
-
- /**
- * Test {@link VersionedTfLauncher#run(ITestInvocationListener)} for test with a null device
- */
- @Test
- public void testRun_nullDevice() {
- mMockIDevice = new NullDevice("null-device-1");
-
- CommandResult cr = new CommandResult(CommandStatus.SUCCESS);
- mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
- mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
- mMockRunUtil.unsetEnvVariable(SubprocessTfLauncher.ANDROID_SERIAL_VAR);
- mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
- mMockRunUtil.setEnvVariable(
- EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE),
- (String) EasyMock.anyObject());
-
- EasyMock.expect(
- mMockRunUtil.runTimedCmd(
- EasyMock.anyLong(),
- (FileOutputStream) EasyMock.anyObject(),
- (FileOutputStream) EasyMock.anyObject(),
- EasyMock.eq("java"),
- (String) EasyMock.anyObject(),
- EasyMock.eq("--add-opens=java.base/java.nio=ALL-UNNAMED"),
- EasyMock.eq("-cp"),
- (String) EasyMock.anyObject(),
- EasyMock.eq("com.android.tradefed.command.CommandRunner"),
- EasyMock.eq(CONFIG_NAME),
- EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
- EasyMock.eq(TF_COMMAND_LINE_TEST),
- EasyMock.eq(TF_COMMAND_LINE_OPTION),
- EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
- EasyMock.eq("--null-device"),
- EasyMock.eq("--" + CommandOptions.INVOCATION_DATA),
- EasyMock.eq(SubprocessTfLauncher.SUBPROCESS_TAG_NAME),
- EasyMock.eq("true"),
- EasyMock.eq("--subprocess-report-file"),
- (String) EasyMock.anyObject()))
- .andReturn(cr);
- Map<ITestDevice, IBuildInfo> deviceInfos = new HashMap<ITestDevice, IBuildInfo>();
- deviceInfos.put(mMockTestDevice, null);
- mVersionedTfLauncher.setDeviceInfos(deviceInfos);
- EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
- EasyMock.expect(mMockBuildInfo.getBuildId()).andReturn("FAKEID").times(2);
- EasyMock.expect(mMockBuildInfo.getFile("general-tests.zip")).andReturn(null);
- mMockBuildInfo.addBuildAttribute(SubprocessTfLauncher.PARENT_PROC_TAG_NAME, "true");
- EasyMock.expect(mMockTestDevice.getIDevice()).andReturn(mMockIDevice).times(1);
- mMockListener.testLog((String)EasyMock.anyObject(), (LogDataType)EasyMock.anyObject(),
- (FileInputStreamSource)EasyMock.anyObject());
- EasyMock.expectLastCall().times(3);
- mMockListener.testRunStarted("StdErr", 1);
- mMockListener.testStarted((TestDescription) EasyMock.anyObject());
- mMockListener.testEnded(
- (TestDescription) EasyMock.anyObject(), EasyMock.eq(new HashMap<String, Metric>()));
- mMockListener.testRunEnded(0, new HashMap<String, Metric>());
-
- EasyMock.expect(mMockConfig.getCommandOptions()).andReturn(new CommandOptions());
- EasyMock.replay(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
- mVersionedTfLauncher.run(mMockListener);
- EasyMock.verify(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
- }
-
- /** Test {@link VersionedTfLauncher#run(ITestInvocationListener)} for test with a StubDevice. */
- @Test
- public void testRun_DeviceNoPreSetup() {
- CommandResult cr = new CommandResult(CommandStatus.SUCCESS);
- mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
- mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
- mMockRunUtil.unsetEnvVariable(SubprocessTfLauncher.ANDROID_SERIAL_VAR);
- mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
- mMockRunUtil.setEnvVariable(
- EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE),
- (String) EasyMock.anyObject());
-
- EasyMock.expect(
- mMockRunUtil.runTimedCmd(
- EasyMock.anyLong(),
- (FileOutputStream) EasyMock.anyObject(),
- (FileOutputStream) EasyMock.anyObject(),
- EasyMock.eq("java"),
- (String) EasyMock.anyObject(),
- EasyMock.eq("--add-opens=java.base/java.nio=ALL-UNNAMED"),
- EasyMock.eq("-cp"),
- (String) EasyMock.anyObject(),
- EasyMock.eq("com.android.tradefed.command.CommandRunner"),
- EasyMock.eq(CONFIG_NAME),
- EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
- EasyMock.eq(TF_COMMAND_LINE_TEST),
- EasyMock.eq(TF_COMMAND_LINE_OPTION),
- EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
- EasyMock.eq("--additional-tests-zip"),
- EasyMock.eq(ADDITIONAL_TEST_ZIP),
- EasyMock.eq("--" + CommandOptions.INVOCATION_DATA),
- EasyMock.eq(SubprocessTfLauncher.SUBPROCESS_TAG_NAME),
- EasyMock.eq("true"),
- EasyMock.eq("--subprocess-report-file"),
- (String) EasyMock.anyObject()))
- .andReturn(cr);
- Map<ITestDevice, IBuildInfo> deviceInfos = new HashMap<ITestDevice, IBuildInfo>();
- deviceInfos.put(mMockTestDevice, null);
- mVersionedTfLauncher.setDeviceInfos(deviceInfos);
- EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
- EasyMock.expect(mMockBuildInfo.getBuildId()).andReturn("FAKEID").times(2);
- EasyMock.expect(mMockBuildInfo.getFile("general-tests.zip"))
- .andReturn(new File(ADDITIONAL_TEST_ZIP));
- mMockBuildInfo.addBuildAttribute(SubprocessTfLauncher.PARENT_PROC_TAG_NAME, "true");
- EasyMock.expect(mMockTestDevice.getIDevice()).andReturn(new StubDevice("serial1")).times(2);
- mMockListener.testLog(
- (String) EasyMock.anyObject(),
- (LogDataType) EasyMock.anyObject(),
- (FileInputStreamSource) EasyMock.anyObject());
- EasyMock.expectLastCall().times(3);
- mMockListener.testRunStarted("StdErr", 1);
- mMockListener.testStarted((TestDescription) EasyMock.anyObject());
- mMockListener.testEnded(
- (TestDescription) EasyMock.anyObject(), EasyMock.eq(new HashMap<String, Metric>()));
- mMockListener.testRunEnded(0, new HashMap<String, Metric>());
-
- EasyMock.expect(mMockConfig.getCommandOptions()).andReturn(new CommandOptions());
- EasyMock.replay(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
- mVersionedTfLauncher.run(mMockListener);
- EasyMock.verify(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
- }
-
- /**
- * Test that when a test is sharded, the instance of the implementation is used and options are
- * passed to the shard test.
- */
- @Test
- public void testGetTestShard() {
- Collection<IRemoteTest> tests = mVersionedTfLauncher.split(2);
- assertEquals(2, tests.size());
- Iterator<IRemoteTest> ite = tests.iterator();
- IRemoteTest firstTest = ite.next();
- assertTrue(firstTest instanceof VersionedTfLauncher);
- IRemoteTest secondTest = ite.next();
- assertTrue(secondTest instanceof VersionedTfLauncher);
-
- VersionedTfLauncher shardedTest = (VersionedTfLauncher) secondTest;
-
- shardedTest.setRunUtil(mMockRunUtil);
- shardedTest.setBuild(mMockBuildInfo);
- shardedTest.setEventStreaming(false);
- shardedTest.setConfiguration(mMockConfig);
-
- mMockIDevice = EasyMock.createMock(IDevice.class);
-
- CommandResult cr = new CommandResult(CommandStatus.SUCCESS);
- mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
- mMockRunUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_SERVER_CONFIG_VARIABLE);
- mMockRunUtil.unsetEnvVariable(SubprocessTfLauncher.ANDROID_SERIAL_VAR);
- mMockRunUtil.setEnvVariablePriority(EnvPriority.SET);
- mMockRunUtil.setEnvVariable(
- EasyMock.eq(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE),
- (String) EasyMock.anyObject());
-
- EasyMock.expect(
- mMockRunUtil.runTimedCmd(
- EasyMock.anyLong(),
- (FileOutputStream) EasyMock.anyObject(),
- (FileOutputStream) EasyMock.anyObject(),
- EasyMock.eq("java"),
- (String) EasyMock.anyObject(),
- EasyMock.eq("--add-opens=java.base/java.nio=ALL-UNNAMED"),
- EasyMock.eq("-cp"),
- (String) EasyMock.anyObject(),
- EasyMock.eq("com.android.tradefed.command.CommandRunner"),
- EasyMock.eq(CONFIG_NAME),
- EasyMock.eq(TF_COMMAND_LINE_TEMPLATE),
- EasyMock.eq(TF_COMMAND_LINE_TEST),
- EasyMock.eq(TF_COMMAND_LINE_OPTION),
- EasyMock.eq(TF_COMMAND_LINE_OPTION_VALUE),
- EasyMock.eq("--serial"),
- EasyMock.eq(FAKE_SERIAL),
- EasyMock.eq("--shard-count"),
- EasyMock.eq("2"),
- EasyMock.eq("--shard-index"),
- EasyMock.eq("1"),
- EasyMock.eq("--" + CommandOptions.INVOCATION_DATA),
- EasyMock.eq(SubprocessTfLauncher.SUBPROCESS_TAG_NAME),
- EasyMock.eq("true"),
- EasyMock.eq("--subprocess-report-file"),
- (String) EasyMock.anyObject()))
- .andReturn(cr);
- Map<ITestDevice, IBuildInfo> deviceInfos = new HashMap<ITestDevice, IBuildInfo>();
- deviceInfos.put(mMockTestDevice, null);
- shardedTest.setDeviceInfos(deviceInfos);
- EasyMock.expect(mMockBuildInfo.getRootDir()).andReturn(new File(""));
- EasyMock.expect(mMockBuildInfo.getBuildId()).andReturn("FAKEID").times(2);
- EasyMock.expect(mMockBuildInfo.getFile("general-tests.zip")).andReturn(null);
- mMockBuildInfo.addBuildAttribute(SubprocessTfLauncher.PARENT_PROC_TAG_NAME, "true");
- EasyMock.expect(mMockTestDevice.getIDevice()).andReturn(mMockIDevice).times(2);
- EasyMock.expect(mMockTestDevice.getSerialNumber()).andReturn(FAKE_SERIAL).times(1);
- mMockListener.testLog(
- (String) EasyMock.anyObject(),
- (LogDataType) EasyMock.anyObject(),
- (FileInputStreamSource) EasyMock.anyObject());
- EasyMock.expectLastCall().times(3);
- mMockListener.testRunStarted("StdErr", 1);
- mMockListener.testStarted((TestDescription) EasyMock.anyObject());
- mMockListener.testEnded(
- (TestDescription) EasyMock.anyObject(), EasyMock.eq(new HashMap<String, Metric>()));
- mMockListener.testRunEnded(0, new HashMap<String, Metric>());
- EasyMock.expect(mMockConfig.getCommandOptions()).andReturn(new CommandOptions());
- EasyMock.replay(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
- shardedTest.run(mMockListener);
- EasyMock.verify(mMockTestDevice, mMockBuildInfo, mMockRunUtil, mMockListener, mMockConfig);
- }
-}
diff --git a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
index 246aeed..cc64b50 100644
--- a/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/binary/ExecutableHostTestTest.java
@@ -19,6 +19,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
@@ -41,8 +42,11 @@
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
import java.io.File;
+import java.io.OutputStream;
import java.util.HashMap;
/** Unit tests for {@link ExecutableHostTest}. */
@@ -101,7 +105,45 @@
CommandResult result = new CommandResult(CommandStatus.SUCCESS);
doReturn(result)
.when(mMockRunUtil)
- .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+ .runTimedCmd(
+ Mockito.anyLong(),
+ (OutputStream) Mockito.any(),
+ Mockito.any(),
+ Mockito.eq(tmpBinary.getAbsolutePath()));
+
+ mExecutableTest.run(mMockListener);
+
+ verify(mMockListener, Mockito.times(1)).testRunStarted(eq(tmpBinary.getName()), eq(1));
+ verify(mMockListener, Mockito.times(0)).testRunFailed(any());
+ verify(mMockListener, Mockito.times(0)).testFailed(any(), any());
+ verify(mMockListener, Mockito.times(1))
+ .testRunEnded(Mockito.anyLong(), Mockito.<HashMap<String, Metric>>any());
+ } finally {
+ FileUtil.recursiveDelete(tmpBinary);
+ }
+ }
+
+ @Test
+ public void testRunHostExecutable_relativePath() throws Exception {
+ File tmpBinary = FileUtil.createTempFile("test-executable", "");
+ try {
+ OptionSetter setter = new OptionSetter(mExecutableTest);
+ setter.setOptionValue("binary", tmpBinary.getAbsolutePath());
+ setter.setOptionValue("relative-path-execution", "true");
+
+ CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+ doReturn(result)
+ .when(mMockRunUtil)
+ .runTimedCmd(
+ Mockito.anyLong(),
+ (OutputStream) Mockito.any(),
+ Mockito.any(),
+ Mockito.eq("bash"),
+ Mockito.eq("-c"),
+ Mockito.eq(
+ String.format(
+ "pushd %s; ./%s;",
+ tmpBinary.getParent(), tmpBinary.getName())));
mExecutableTest.run(mMockListener);
@@ -125,7 +167,11 @@
CommandResult result = new CommandResult(CommandStatus.SUCCESS);
doReturn(result)
.when(mMockRunUtil)
- .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+ .runTimedCmd(
+ Mockito.anyLong(),
+ (OutputStream) Mockito.any(),
+ Mockito.any(),
+ Mockito.eq(tmpBinary.getAbsolutePath()));
doThrow(new DeviceNotAvailableException()).when(mMockDevice).waitForDeviceAvailable();
try {
@@ -165,7 +211,11 @@
CommandResult result = new CommandResult(CommandStatus.SUCCESS);
doReturn(result)
.when(mMockRunUtil)
- .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+ .runTimedCmd(
+ Mockito.anyLong(),
+ (OutputStream) Mockito.any(),
+ Mockito.any(),
+ Mockito.eq(tmpBinary.getAbsolutePath()));
mExecutableTest.run(mMockListener);
@@ -222,9 +272,24 @@
CommandResult result = new CommandResult(CommandStatus.FAILED);
result.setExitCode(5);
result.setStdout("stdout");
- doReturn(result)
+
+ doAnswer(
+ new Answer<CommandResult>() {
+
+ @Override
+ public CommandResult answer(InvocationOnMock invocation)
+ throws Throwable {
+ OutputStream outputStream = invocation.getArgument(1);
+ outputStream.write("stdout".getBytes());
+ return result;
+ }
+ })
.when(mMockRunUtil)
- .runTimedCmd(Mockito.anyLong(), Mockito.eq(tmpBinary.getAbsolutePath()));
+ .runTimedCmd(
+ Mockito.anyLong(),
+ (OutputStream) Mockito.any(),
+ Mockito.any(),
+ Mockito.eq(tmpBinary.getAbsolutePath()));
mExecutableTest.run(mMockListener);
diff --git a/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java b/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
index 4d98090..9f91e7e 100644
--- a/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
+++ b/tests/src/com/android/tradefed/testtype/junit4/BaseHostJUnit4TestTest.java
@@ -15,6 +15,8 @@
*/
package com.android.tradefed.testtype.junit4;
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -44,6 +46,7 @@
import com.android.tradefed.util.ListInstrumentationParser;
import org.easymock.EasyMock;
+import org.easymock.IAnswer;
import org.junit.Assert;
import org.junit.AssumptionViolatedException;
import org.junit.Before;
@@ -187,6 +190,57 @@
EasyMock.verify(mMockBuild, mMockDevice);
}
+ /** Test that we carry the assumption failure messages. */
+ @Test
+ public void testRunDeviceTests_assumptionFailure() throws Exception {
+ TestableHostJUnit4Test test = new TestableHostJUnit4Test();
+ test.setDevice(mMockDevice);
+ test.setBuild(mMockBuild);
+ test.setInvocationContext(mMockContext);
+ mMockDevice.executeShellCommand(
+ EasyMock.eq("pm list instrumentation"), EasyMock.anyObject());
+ EasyMock.expect(mMockDevice.getIDevice()).andReturn(new StubDevice("serial"));
+ EasyMock.expect(
+ mMockDevice.runInstrumentationTests(
+ (IRemoteAndroidTestRunner) EasyMock.anyObject(),
+ EasyMock.<Collection<ITestLifeCycleReceiver>>anyObject()))
+ .andAnswer(
+ new IAnswer<Boolean>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public Boolean answer() throws Throwable {
+ Collection<ITestLifeCycleReceiver> receivers =
+ (Collection<ITestLifeCycleReceiver>)
+ getCurrentArguments()[1];
+ for (ITestLifeCycleReceiver i : receivers) {
+ i.testRunStarted("runName", 2);
+ i.testStarted(new TestDescription("class", "test1"));
+ i.testAssumptionFailure(
+ new TestDescription("class", "test1"), "assumpFail");
+ i.testEnded(
+ new TestDescription("class", "test1"),
+ new HashMap<String, Metric>());
+
+ i.testStarted(new TestDescription("class", "test2"));
+ i.testAssumptionFailure(
+ new TestDescription("class", "test2"), "assumpFail2");
+ i.testEnded(
+ new TestDescription("class", "test2"),
+ new HashMap<String, Metric>());
+ }
+ return true;
+ }
+ });
+ EasyMock.replay(mMockBuild, mMockDevice);
+ try {
+ test.runDeviceTests("com.package", "testClass");
+ fail("Should have thrown an Assume exception.");
+ } catch (AssumptionViolatedException e) {
+ assertEquals("assumpFail\n\nassumpFail2", e.getMessage());
+ }
+ EasyMock.verify(mMockBuild, mMockDevice);
+ }
+
/** Test that when running an instrumentation, the abi is properly passed. */
@Test
public void testRunDeviceTests_abi() throws Exception {
diff --git a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
index 6fea16b..181154d 100644
--- a/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
+++ b/tests/src/com/android/tradefed/testtype/python/PythonBinaryHostTestTest.java
@@ -97,7 +97,11 @@
mMockRunUtil.runTimedCmd(
EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
.andReturn(res);
- mMockListener.testRunStarted(binary.getName(), 5);
+ mMockListener.testRunStarted(
+ EasyMock.eq(binary.getName()),
+ EasyMock.eq(5),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
mMockListener.testLog(
EasyMock.eq(binary.getName() + "-stderr"),
EasyMock.eq(LogDataType.TEXT),
@@ -137,7 +141,11 @@
mMockRunUtil.runTimedCmd(
EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
.andReturn(res);
- mMockListener.testRunStarted(binary.getName(), 5);
+ mMockListener.testRunStarted(
+ EasyMock.eq(binary.getName()),
+ EasyMock.eq(5),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
mMockListener.testLog(
EasyMock.eq(binary.getName() + "-stderr"),
EasyMock.eq(LogDataType.TEXT),
@@ -212,7 +220,11 @@
mMockRunUtil.runTimedCmd(
EasyMock.anyLong(), EasyMock.eq(binary.getAbsolutePath())))
.andReturn(res);
- mMockListener.testRunStarted(binary.getName(), 5);
+ mMockListener.testRunStarted(
+ EasyMock.eq(binary.getName()),
+ EasyMock.eq(5),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
mMockListener.testLog(
EasyMock.eq(binary.getName() + "-stderr"),
EasyMock.eq(LogDataType.TEXT),
diff --git a/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java b/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java
new file mode 100644
index 0000000..f71c4e4
--- /dev/null
+++ b/tests/src/com/android/tradefed/testtype/retry/ResultAggregatorTest.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.testtype.retry;
+
+import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.config.ConfigurationDef;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ILogSaver;
+import com.android.tradefed.result.ILogSaverListener;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.retry.ISupportGranularResults;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+/** Unit tests for {@link ResultAggregator}. */
+@RunWith(JUnit4.class)
+public class ResultAggregatorTest {
+
+ private ResultAggregator mAggregator;
+ private ILogSaverListener mAggListener;
+ private ITestDetailedReceiver mDetailedListener;
+ private IInvocationContext mInvocationContext;
+ private IInvocationContext mModuleContext;
+
+ private interface ITestDetailedReceiver
+ extends ISupportGranularResults, ITestInvocationListener, ILogSaverListener {}
+
+ @Before
+ public void setUp() {
+ mAggListener = EasyMock.createMock(ILogSaverListener.class);
+ mDetailedListener = EasyMock.createMock(ITestDetailedReceiver.class);
+ mInvocationContext = new InvocationContext();
+ mInvocationContext.addDeviceBuildInfo(
+ ConfigurationDef.DEFAULT_DEVICE_NAME, new BuildInfo());
+ mModuleContext = new InvocationContext();
+ }
+
+ @Test
+ public void testForwarding() {
+ TestDescription test1 = new TestDescription("classname", "test1");
+ TestDescription test2 = new TestDescription("classname", "test2");
+ ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+ EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+ // Invocation level
+ mAggListener.setLogSaver(logger);
+ mAggListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+ mDetailedListener.setLogSaver(logger);
+ mDetailedListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+ mAggListener.testModuleStarted(mModuleContext);
+ mDetailedListener.testModuleStarted(mModuleContext);
+
+ // Detailed receives the breakdown
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testFailed(test2, "I failed. retry me.");
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunFailed("run fail");
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Aggregated listeners receives the aggregated results
+ mAggListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggListener.testModuleEnded();
+ mDetailedListener.testModuleEnded();
+ mAggListener.invocationEnded(500L);
+ mDetailedListener.invocationEnded(500L);
+
+ EasyMock.replay(mAggListener, mDetailedListener);
+ mAggregator =
+ new ResultAggregator(
+ Arrays.asList(mAggListener, mDetailedListener),
+ RetryStrategy.RETRY_ANY_FAILURE);
+ mAggregator.setLogSaver(logger);
+ mAggregator.invocationStarted(mInvocationContext);
+ mAggregator.testModuleStarted(mModuleContext);
+ // Attempt 1
+ mAggregator.testRunStarted("run1", 2, 0);
+ mAggregator.testStarted(test1);
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testStarted(test2);
+ mAggregator.testFailed(test2, "I failed. retry me.");
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunFailed("run fail");
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ // Attempt 2
+ mAggregator.testRunStarted("run1", 2, 1);
+ mAggregator.testStarted(test2);
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggregator.testModuleEnded();
+ mAggregator.invocationEnded(500L);
+ EasyMock.verify(mAggListener, mDetailedListener);
+ }
+
+ @Test
+ public void testForwarding_noModules() {
+ TestDescription test1 = new TestDescription("classname", "test1");
+ TestDescription test2 = new TestDescription("classname", "test2");
+ ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+ EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+ // Invocation level
+ mAggListener.setLogSaver(logger);
+ mAggListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+ mDetailedListener.setLogSaver(logger);
+ mDetailedListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+ // Detailed receives the breakdown
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testFailed(test2, "I failed. retry me.");
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Aggregated listeners receives the aggregated results
+ mAggListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testRunEnded(900L, new HashMap<String, Metric>());
+
+ mAggListener.invocationEnded(500L);
+ mDetailedListener.invocationEnded(500L);
+
+ EasyMock.replay(mAggListener, mDetailedListener);
+ mAggregator =
+ new ResultAggregator(
+ Arrays.asList(mAggListener, mDetailedListener),
+ RetryStrategy.RETRY_ANY_FAILURE);
+ mAggregator.setLogSaver(logger);
+ mAggregator.invocationStarted(mInvocationContext);
+ // Attempt 1
+ mAggregator.testRunStarted("run1", 2, 0);
+ mAggregator.testStarted(test1);
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testStarted(test2);
+ mAggregator.testFailed(test2, "I failed. retry me.");
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ // Attempt 2
+ mAggregator.testRunStarted("run1", 2, 1);
+ mAggregator.testStarted(test2);
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggregator.invocationEnded(500L);
+ EasyMock.verify(mAggListener, mDetailedListener);
+ }
+
+ /** Test aggregation of results coming from a module first then from a simple test run. */
+ @Test
+ public void testForwarding_module_noModule() {
+ TestDescription test1 = new TestDescription("classname", "test1");
+ TestDescription test2 = new TestDescription("classname", "test2");
+ ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+ EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+ // Invocation level
+ mAggListener.setLogSaver(logger);
+ mAggListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+ mDetailedListener.setLogSaver(logger);
+ mDetailedListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+ mAggListener.testModuleStarted(mModuleContext);
+ mDetailedListener.testModuleStarted(mModuleContext);
+
+ // Detailed receives the breakdown
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testFailed(test2, "I failed. retry me.");
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Aggregated listeners receives the aggregated results
+ mAggListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggListener.testModuleEnded();
+ mDetailedListener.testModuleEnded();
+
+ // Detailed receives the breakdown for non-module
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testFailed(test1, "I failed. retry me.");
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(1), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Aggregated listeners receives the aggregated results for non-module
+ mAggListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+ mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testRunEnded(900L, new HashMap<String, Metric>());
+
+ mAggListener.invocationEnded(500L);
+ mDetailedListener.invocationEnded(500L);
+
+ EasyMock.replay(mAggListener, mDetailedListener);
+ mAggregator =
+ new ResultAggregator(
+ Arrays.asList(mAggListener, mDetailedListener),
+ RetryStrategy.RETRY_ANY_FAILURE);
+ mAggregator.setLogSaver(logger);
+ mAggregator.invocationStarted(mInvocationContext);
+ mAggregator.testModuleStarted(mModuleContext);
+ // Attempt 1
+ mAggregator.testRunStarted("run1", 2, 0);
+ mAggregator.testStarted(test1);
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testStarted(test2);
+ mAggregator.testFailed(test2, "I failed. retry me.");
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ // Attempt 2
+ mAggregator.testRunStarted("run1", 2, 1);
+ mAggregator.testStarted(test2);
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ mAggregator.testModuleEnded();
+
+ // New run that is not a module
+ mAggregator.testRunStarted("run2", 1, 0);
+ mAggregator.testStarted(test1);
+ mAggregator.testFailed(test1, "I failed. retry me.");
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggregator.testRunStarted("run2", 1, 1);
+ mAggregator.testStarted(test1);
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggregator.invocationEnded(500L);
+ EasyMock.verify(mAggListener, mDetailedListener);
+ }
+
+ /** Test aggregation of results coming from a simple test run first then from a module. */
+ @Test
+ public void testForwarding_noModule_module() {
+ TestDescription test1 = new TestDescription("classname", "test1");
+ TestDescription test2 = new TestDescription("classname", "test2");
+ ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+ EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+ // Invocation level
+ mAggListener.setLogSaver(logger);
+ mAggListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+ mDetailedListener.setLogSaver(logger);
+ mDetailedListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+ mAggListener.testModuleStarted(mModuleContext);
+ mDetailedListener.testModuleStarted(mModuleContext);
+
+ // Detailed receives the breakdown
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testFailed(test2, "I failed. retry me.");
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Aggregated listeners receives the aggregated results
+ mAggListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggListener.testModuleEnded();
+ mDetailedListener.testModuleEnded();
+
+ // Detailed receives the breakdown for non-module
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testFailed(test1, "I failed. retry me.");
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(1), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Aggregated listeners receives the aggregated results for non-module
+ mAggListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+ mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testRunEnded(900L, new HashMap<String, Metric>());
+
+ mAggListener.invocationEnded(500L);
+ mDetailedListener.invocationEnded(500L);
+
+ EasyMock.replay(mAggListener, mDetailedListener);
+ mAggregator =
+ new ResultAggregator(
+ Arrays.asList(mAggListener, mDetailedListener),
+ RetryStrategy.RETRY_ANY_FAILURE);
+ mAggregator.setLogSaver(logger);
+ mAggregator.invocationStarted(mInvocationContext);
+ // First run that is not a module
+ mAggregator.testRunStarted("run2", 1, 0);
+ mAggregator.testStarted(test1);
+ mAggregator.testFailed(test1, "I failed. retry me.");
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggregator.testRunStarted("run2", 1, 1);
+ mAggregator.testStarted(test1);
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Module start
+ mAggregator.testModuleStarted(mModuleContext);
+ // Attempt 1
+ mAggregator.testRunStarted("run1", 2, 0);
+ mAggregator.testStarted(test1);
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testStarted(test2);
+ mAggregator.testFailed(test2, "I failed. retry me.");
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ // Attempt 2
+ mAggregator.testRunStarted("run1", 2, 1);
+ mAggregator.testStarted(test2);
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ mAggregator.testModuleEnded();
+
+ mAggregator.invocationEnded(500L);
+ EasyMock.verify(mAggListener, mDetailedListener);
+ }
+
+ /** Test when two modules follow each others. */
+ @Test
+ public void testForwarding_module_module() {
+ TestDescription test1 = new TestDescription("classname", "test1");
+ TestDescription test2 = new TestDescription("classname", "test2");
+ ILogSaver logger = EasyMock.createMock(ILogSaver.class);
+
+ EasyMock.expect(mDetailedListener.supportGranularResults()).andStubReturn(true);
+
+ // Invocation level
+ mAggListener.setLogSaver(logger);
+ mAggListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mAggListener.getSummary()).andStubReturn(null);
+ mDetailedListener.setLogSaver(logger);
+ mDetailedListener.invocationStarted(mInvocationContext);
+ EasyMock.expect(mDetailedListener.getSummary()).andStubReturn(null);
+
+ mAggListener.testModuleStarted(mModuleContext);
+ mDetailedListener.testModuleStarted(mModuleContext);
+
+ // Detailed receives the breakdown
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testFailed(test2, "I failed. retry me.");
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(1), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Aggregated listeners receives the aggregated results
+ mAggListener.testRunStarted(
+ EasyMock.eq("run1"), EasyMock.eq(2), EasyMock.eq(0), EasyMock.anyLong());
+ mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testStarted(EasyMock.eq(test2), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test2),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggListener.testModuleEnded();
+ mDetailedListener.testModuleEnded();
+
+ // second module
+ mAggListener.testModuleStarted(mModuleContext);
+ mDetailedListener.testModuleStarted(mModuleContext);
+
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testFailed(test1, "I failed. retry me.");
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+ mDetailedListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(1), EasyMock.anyLong());
+ mDetailedListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mDetailedListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mDetailedListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ // Aggregated listeners receives the aggregated results for non-module
+ mAggListener.testRunStarted(
+ EasyMock.eq("run2"), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
+ mAggListener.testStarted(EasyMock.eq(test1), EasyMock.anyLong());
+ mAggListener.testEnded(
+ EasyMock.eq(test1),
+ EasyMock.anyLong(),
+ EasyMock.<HashMap<String, Metric>>anyObject());
+ mAggListener.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggListener.testModuleEnded();
+ mDetailedListener.testModuleEnded();
+
+ mAggListener.invocationEnded(500L);
+ mDetailedListener.invocationEnded(500L);
+
+ EasyMock.replay(mAggListener, mDetailedListener);
+ mAggregator =
+ new ResultAggregator(
+ Arrays.asList(mAggListener, mDetailedListener),
+ RetryStrategy.RETRY_ANY_FAILURE);
+ mAggregator.setLogSaver(logger);
+ mAggregator.invocationStarted(mInvocationContext);
+
+ // Module 1 starts
+ mAggregator.testModuleStarted(mModuleContext);
+ // Attempt 1
+ mAggregator.testRunStarted("run1", 2, 0);
+ mAggregator.testStarted(test1);
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testStarted(test2);
+ mAggregator.testFailed(test2, "I failed. retry me.");
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ // Attempt 2
+ mAggregator.testRunStarted("run1", 2, 1);
+ mAggregator.testStarted(test2);
+ mAggregator.testEnded(test2, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ mAggregator.testModuleEnded();
+
+ // Module 2 starts
+ mAggregator.testModuleStarted(mModuleContext);
+ mAggregator.testRunStarted("run2", 1, 0);
+ mAggregator.testStarted(test1);
+ mAggregator.testFailed(test1, "I failed. retry me.");
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+
+ mAggregator.testRunStarted("run2", 1, 1);
+ mAggregator.testStarted(test1);
+ mAggregator.testEnded(test1, new HashMap<String, Metric>());
+ mAggregator.testRunEnded(450L, new HashMap<String, Metric>());
+ mAggregator.testModuleEnded();
+
+ mAggregator.invocationEnded(500L);
+ EasyMock.verify(mAggListener, mDetailedListener);
+ }
+}
diff --git a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
index df7bfd2..659391e 100644
--- a/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/BaseTestSuiteTest.java
@@ -30,6 +30,7 @@
import com.android.tradefed.config.OptionSetter;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.ITestLogger;
import com.android.tradefed.testtype.Abi;
import com.android.tradefed.testtype.IAbi;
import com.android.tradefed.testtype.IRemoteTest;
@@ -286,6 +287,29 @@
assertNull(mRunner.split(2));
}
+ /** Ensure that during sharding we don't attempt to log the filter files. */
+ @Test
+ public void testSplit_withFilters() throws Exception {
+ OptionSetter setter = new OptionSetter(mRunner);
+ setter.setOptionValue("suite-config-prefix", "suite");
+ setter.setOptionValue("run-suite-tag", "example-suite");
+ Set<String> excludeModule = new HashSet<>();
+ for (int i = 0; i < 25; i++) {
+ excludeModule.add("arm64-v8a suite/load-filter-test" + i);
+ }
+ mRunner.setExcludeFilter(excludeModule);
+ ITestLogger logger = EasyMock.createMock(ITestLogger.class);
+ mRunner.setTestLogger(logger);
+
+ EasyMock.replay(logger);
+ Collection<IRemoteTest> tests = mRunner.split(2);
+ assertEquals(4, tests.size());
+ for (IRemoteTest test : tests) {
+ assertTrue(test instanceof BaseTestSuite);
+ }
+ EasyMock.verify(logger);
+ }
+
/**
* Test for {@link BaseTestSuite#loadTests()} that when a test config supports IAbiReceiver,
* multiple instances of the config are queued up.
@@ -648,4 +672,51 @@
}
EasyMock.verify(mockDevice);
}
+
+ /** Test that loading the option parameterization is gated by the option. */
+ @Test
+ public void testLoadTests_optionalParameterizedModule() throws Exception {
+ ITestDevice mockDevice = EasyMock.createMock(ITestDevice.class);
+ mRunner.setDevice(mockDevice);
+ OptionSetter setter = new OptionSetter(mRunner);
+ setter.setOptionValue("suite-config-prefix", "suite");
+ setter.setOptionValue("run-suite-tag", "example-suite-parameters");
+ setter.setOptionValue("enable-parameterized-modules", "true");
+ setter.setOptionValue("enable-optional-parameterization", "true");
+ setter.setOptionValue(
+ "test-arg",
+ "com.android.tradefed.testtype.suite.TestSuiteStub:"
+ + "exclude-annotation:android.platform.test.annotations.AppModeInstant");
+ EasyMock.replay(mockDevice);
+ LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+ assertEquals(4, configMap.size());
+ assertTrue(configMap.containsKey("arm64-v8a suite/stub-parameterized"));
+ assertTrue(configMap.containsKey("arm64-v8a suite/stub-parameterized[instant]"));
+ assertTrue(configMap.containsKey("arm64-v8a suite/stub-parameterized[secondary_user]"));
+ assertTrue(configMap.containsKey("armeabi-v7a suite/stub-parameterized"));
+ EasyMock.verify(mockDevice);
+ }
+
+ /** Test that we can explicitly request the option parameterization type. */
+ @Test
+ public void testLoadTests_optionalParameterizedModule_filter() throws Exception {
+ ITestDevice mockDevice = EasyMock.createMock(ITestDevice.class);
+ mRunner.setDevice(mockDevice);
+ OptionSetter setter = new OptionSetter(mRunner);
+ setter.setOptionValue("suite-config-prefix", "suite");
+ setter.setOptionValue("run-suite-tag", "example-suite-parameters");
+ setter.setOptionValue("enable-parameterized-modules", "true");
+ setter.setOptionValue("enable-optional-parameterization", "true");
+ setter.setOptionValue("module-parameter", "SECONDARY_USER");
+ setter.setOptionValue(
+ "test-arg",
+ "com.android.tradefed.testtype.suite.TestSuiteStub:"
+ + "exclude-annotation:android.platform.test.annotations.AppModeInstant");
+ EasyMock.replay(mockDevice);
+ LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+ // Only the secondary_user requested is created
+ assertEquals(1, configMap.size());
+ assertTrue(configMap.containsKey("arm64-v8a suite/stub-parameterized[secondary_user]"));
+ EasyMock.verify(mockDevice);
+ }
}
diff --git a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
index c941afd..b64498b 100644
--- a/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/GranularRetriableTestWrapperTest.java
@@ -15,14 +15,12 @@
*/
package com.android.tradefed.testtype.suite;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.config.IConfiguration;
+import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.DeviceUnresponsiveException;
import com.android.tradefed.device.ITestDevice;
@@ -30,6 +28,7 @@
import com.android.tradefed.device.metric.DeviceMetricData;
import com.android.tradefed.device.metric.IMetricCollector;
import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.CollectingTestListener;
import com.android.tradefed.result.FileSystemLogSaver;
@@ -39,7 +38,8 @@
import com.android.tradefed.result.TestRunResult;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.ITestFilterReceiver;
-import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
+import com.android.tradefed.testtype.retry.RetryStatistics;
+import com.android.tradefed.testtype.retry.RetryStrategy;
import org.easymock.EasyMock;
import org.junit.Before;
@@ -245,14 +245,7 @@
private GranularRetriableTestWrapper createGranularTestWrapper(
IRemoteTest test, int maxRunCount, List<IMetricCollector> collectors) {
GranularRetriableTestWrapper granularTestWrapper =
- new GranularRetriableTestWrapper(test, null, null, null, maxRunCount) {
- @Override
- List<IMetricCollector> cloneCollectors(
- List<IMetricCollector> originalCollectors) {
- // For testing purpose, avoid cloning.
- return originalCollectors;
- }
- };
+ new GranularRetriableTestWrapper(test, null, null, null, maxRunCount);
granularTestWrapper.setModuleId("test module");
granularTestWrapper.setMarkTestsSkipped(false);
granularTestWrapper.setMetricCollectors(collectors);
@@ -282,7 +275,7 @@
.run(Mockito.any(ITestInvocationListener.class));
GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(mockTest, 1);
- granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+ granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
try {
granularTestWrapper.run(new CollectingTestListener());
fail("Should have thrown an exception.");
@@ -334,7 +327,7 @@
int maxRunCount = 5;
GranularRetriableTestWrapper granularTestWrapper =
createGranularTestWrapper(test, maxRunCount);
- granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+ granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
granularTestWrapper.run(new CollectingTestListener());
// Verify the test runs several times but under the same run name
assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -370,8 +363,9 @@
}
// Since tests stay failed, we have two failure in our monitoring.
- assertEquals(0, granularTestWrapper.getRetrySuccess());
- assertEquals(2, granularTestWrapper.getRetryFailed());
+ RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+ assertEquals(0, stats.mRetrySuccess);
+ assertEquals(2, stats.mRetryFailure);
}
/** Test when a test becomes pass after failing */
@@ -393,7 +387,7 @@
int maxRunCount = 5;
GranularRetriableTestWrapper granularTestWrapper =
createGranularTestWrapper(test, maxRunCount);
- granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+ granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
granularTestWrapper.run(new CollectingTestListener());
// Verify the test runs several times but under the same run name
assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -429,8 +423,9 @@
}
// One success since one test recover, one test never recover so one failure
- assertEquals(1, granularTestWrapper.getRetrySuccess());
- assertEquals(1, granularTestWrapper.getRetryFailed());
+ RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+ assertEquals(1, stats.mRetrySuccess);
+ assertEquals(1, stats.mRetryFailure);
}
/** Test when all tests become pass, we stop intra-module retry early. */
@@ -453,7 +448,7 @@
int maxRunCount = 5;
GranularRetriableTestWrapper granularTestWrapper =
createGranularTestWrapper(test, maxRunCount);
- granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+ granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
granularTestWrapper.run(new CollectingTestListener());
// Verify the test runs several times but under the same run name
assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -516,9 +511,10 @@
.getTestResults()
.containsKey(fakeTestCase2));
- // One success since one test recover, one test never recover so one failure
- assertEquals(2, granularTestWrapper.getRetrySuccess());
- assertEquals(0, granularTestWrapper.getRetryFailed());
+ // One success since one test recover, one test never recover so one failure\
+ RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+ assertEquals(2, stats.mRetrySuccess);
+ assertEquals(0, stats.mRetryFailure);
}
/**
@@ -539,7 +535,7 @@
int maxRunCount = 3;
GranularRetriableTestWrapper granularTestWrapper =
createGranularTestWrapper(test, maxRunCount);
- granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+ granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
granularTestWrapper.run(new CollectingTestListener());
assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -579,7 +575,7 @@
GranularRetriableTestWrapper granularTestWrapper =
createGranularTestWrapper(test, maxRunCount);
- granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_CASE_FAILURE);
+ granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
granularTestWrapper.run(new CollectingTestListener());
// Two runs.
assertEquals(2, granularTestWrapper.getTestRunResultCollected().size());
@@ -620,7 +616,7 @@
FakeTest test = new FakeTest(testCases);
test.setRunFailure("I failed!");
GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 3);
- granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_RUN_FAILURE);
+ granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
granularTestWrapper.run(new CollectingTestListener());
assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -636,8 +632,9 @@
}
// No Test cases tracking since it was a run retry.
- assertEquals(0, granularTestWrapper.getRetrySuccess());
- assertEquals(0, granularTestWrapper.getRetryFailed());
+ RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+ assertEquals(0, stats.mRetrySuccess);
+ assertEquals(0, stats.mRetryFailure);
}
/**
@@ -654,10 +651,8 @@
FakeTest test = new FakeTest(testCases);
test.setRunFailure("I failed!");
test.setClearRunFailure(3);
- // Failed test cases do not affect retry of runs
- test.addFailedTestCase(fakeTestCase1);
GranularRetriableTestWrapper granularTestWrapper = createGranularTestWrapper(test, 7);
- granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_TEST_RUN_FAILURE);
+ granularTestWrapper.setRetryStrategy(RetryStrategy.RETRY_ANY_FAILURE);
granularTestWrapper.run(new CollectingTestListener());
assertEquals(1, granularTestWrapper.getTestRunResultCollected().size());
@@ -677,8 +672,9 @@
assertEquals(2, lastRes.getNumCompleteTests());
// No Test cases tracking since it was a run retry.
- assertEquals(0, granularTestWrapper.getRetrySuccess());
- assertEquals(0, granularTestWrapper.getRetryFailed());
+ RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+ assertEquals(0, stats.mRetrySuccess);
+ assertEquals(0, stats.mRetryFailure);
}
/** Test the retry with iterations, it doesn't require any failure to rerun. */
@@ -707,8 +703,9 @@
}
// No Test cases tracking since it was a run retry.
- assertEquals(0, granularTestWrapper.getRetrySuccess());
- assertEquals(0, granularTestWrapper.getRetryFailed());
+ RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+ assertEquals(0, stats.mRetrySuccess);
+ assertEquals(0, stats.mRetryFailure);
}
/** When re-running until failure, stop when failure is encountered. */
@@ -737,9 +734,9 @@
// All tests cases are rerun each time.
assertEquals(2, res.getNumCompleteTests());
- // No Test cases tracking since it was a run retry.
- assertEquals(0, granularTestWrapper.getRetrySuccess());
- assertEquals(0, granularTestWrapper.getRetryFailed());
+ // No stats since no retry occurred.
+ RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+ assertNull(stats);
}
/**
@@ -752,8 +749,10 @@
// Add a disabled collector to ensure it's never called
CalledMetricCollector notCalledCollector = new CalledMetricCollector();
notCalledCollector.setDisable(true);
+ notCalledCollector.mName = "not-called";
collectors.add(notCalledCollector);
CalledMetricCollector calledCollector = new CalledMetricCollector();
+ calledCollector.mName = "called";
collectors.add(calledCollector);
ArrayList<TestDescription> testCases = new ArrayList<>();
@@ -789,12 +788,16 @@
// All tests cases are rerun each time.
assertEquals(2, lastRes.getNumCompleteTests());
assertEquals(1, lastRes.getFailedTests().size());
+ assertTrue(lastRes.getRunProtoMetrics().containsKey("called"));
+ assertFalse(lastRes.getRunProtoMetrics().containsKey("not-called"));
lastRes = allResults.get(4);
assertFalse(lastRes.isRunFailure());
// The passed test does not rerun now that there is no run failure.
assertEquals(1, lastRes.getNumCompleteTests());
assertEquals(1, lastRes.getFailedTests().size());
+ assertTrue(lastRes.getRunProtoMetrics().containsKey("called"));
+ assertFalse(lastRes.getRunProtoMetrics().containsKey("not-called"));
lastRes = allResults.get(5);
assertFalse(lastRes.isRunFailure());
@@ -802,14 +805,13 @@
assertEquals(1, lastRes.getNumCompleteTests());
// The failed test final pass
assertEquals(0, lastRes.getFailedTests().size());
+ assertTrue(lastRes.getRunProtoMetrics().containsKey("called"));
+ assertFalse(lastRes.getRunProtoMetrics().containsKey("not-called"));
- // No Test cases tracking since it was a run retry.
- assertEquals(1, granularTestWrapper.getRetrySuccess());
- assertEquals(0, granularTestWrapper.getRetryFailed());
-
- // Ensure that the disabled collector was not called, and enabled one was called
- assertFalse(notCalledCollector.wasCalled);
- assertTrue(calledCollector.wasCalled);
+ // Check that failure are cleared
+ RetryStatistics stats = granularTestWrapper.getRetryStatistics();
+ assertEquals(1, stats.mRetrySuccess);
+ assertEquals(0, stats.mRetryFailure);
}
/**
@@ -862,28 +864,41 @@
/** Collector that track if it was called or not */
public static class CalledMetricCollector extends BaseDeviceMetricCollector {
- public boolean wasCalled = false;
+ @Option(name = "name")
+ public String mName;
@Override
public void onTestRunStart(DeviceMetricData runData) {
- wasCalled = true;
+ runData.addMetric(
+ mName,
+ Metric.newBuilder()
+ .setMeasurements(Measurements.newBuilder().setSingleString(mName)));
}
@Override
public void onTestRunEnd(
DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
- wasCalled = true;
+ runData.addMetric(
+ mName,
+ Metric.newBuilder()
+ .setMeasurements(Measurements.newBuilder().setSingleString(mName)));
}
@Override
public void onTestStart(DeviceMetricData testData) {
- wasCalled = true;
+ testData.addMetric(
+ mName,
+ Metric.newBuilder()
+ .setMeasurements(Measurements.newBuilder().setSingleString(mName)));
}
@Override
public void onTestEnd(
DeviceMetricData testData, final Map<String, Metric> currentTestCaseMetrics) {
- wasCalled = true;
+ testData.addMetric(
+ mName,
+ Metric.newBuilder()
+ .setMeasurements(Measurements.newBuilder().setSingleString(mName)));
}
}
}
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteIntegrationTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteIntegrationTest.java
index 11e2593..b7c8742 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteIntegrationTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteIntegrationTest.java
@@ -504,7 +504,7 @@
mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockBuildInfo);
StrictShardHelper helper = new StrictShardHelper();
- helper.shardConfig(config, mContext, new TestShardRescheduler());
+ helper.shardConfig(config, mContext, new TestShardRescheduler(), null);
assertEquals(2, mListener.getTotalModules());
assertEquals(2, mListener.getCompleteModules());
@@ -545,7 +545,7 @@
StrictShardHelper helper = new StrictShardHelper();
TestParallelShardRescheduler rescheduler = new TestParallelShardRescheduler();
- helper.shardConfig(config, mContext, rescheduler);
+ helper.shardConfig(config, mContext, rescheduler, null);
// Wait until all results are received, we expect 2 modules.
while (mListener.getTotalModules() < 2) {
for (Thread t : rescheduler.mRunning) {
@@ -591,7 +591,7 @@
mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockBuildInfo);
StrictShardHelper helper = new StrictShardHelper();
- helper.shardConfig(config, mContext, null);
+ helper.shardConfig(config, mContext, null, null);
// rescheduler is not called, execution is in the same invocation.
new ResultForwarder(config.getTestInvocationListeners()).invocationStarted(mContext);
for (IRemoteTest test : config.getTests()) {
@@ -670,7 +670,7 @@
mContext.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockBuildInfo);
StrictShardHelper helper = new StrictShardHelper();
- helper.shardConfig(config, mContext, null);
+ helper.shardConfig(config, mContext, null, null);
// rescheduler is not called, execution is in the same invocation.
new ResultForwarder(config.getTestInvocationListeners()).invocationStarted(mContext);
for (IRemoteTest test : config.getTests()) {
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteMultiTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteMultiTest.java
index ae27598..1759f0a 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteMultiTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteMultiTest.java
@@ -116,6 +116,7 @@
mMockDevice1 = EasyMock.createMock(ITestDevice.class);
EasyMock.expect(mMockDevice1.getSerialNumber()).andStubReturn("SERIAL1");
mMockBuildInfo1 = EasyMock.createMock(IBuildInfo.class);
+ EasyMock.expect(mMockBuildInfo1.getRemoteFiles()).andReturn(null).once();
mMockDevice2 = EasyMock.createMock(ITestDevice.class);
EasyMock.expect(mMockDevice2.getSerialNumber()).andStubReturn("SERIAL2");
mMockBuildInfo2 = EasyMock.createMock(IBuildInfo.class);
diff --git a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
index 8374a80..62b3512 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ITestSuiteTest.java
@@ -15,6 +15,7 @@
*/
package com.android.tradefed.testtype.suite;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -22,12 +23,15 @@
import static org.junit.Assert.fail;
import com.android.ddmlib.IDevice;
+import com.android.tradefed.build.BuildInfo;
import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IDeviceBuildInfo;
import com.android.tradefed.config.Configuration;
import com.android.tradefed.config.ConfigurationDef;
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.ConfigurationFactory;
+import com.android.tradefed.config.DynamicRemoteFileResolver;
import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionSetter;
@@ -77,7 +81,9 @@
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
+import java.io.File;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
@@ -143,6 +149,7 @@
config.setTargetPreparer(mPreparer);
}
config.setTest(new StubCollectingTest());
+ config.getConfigurationDescription().setModuleName(TEST_CONFIG_NAME);
testConfig.put(TEST_CONFIG_NAME, config);
for (int i = 1; i < mNumTests; i++) {
@@ -269,6 +276,7 @@
EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("SERIAL");
EasyMock.expect(mMockDevice.getIDevice()).andStubReturn(EasyMock.createMock(IDevice.class));
mMockBuildInfo = EasyMock.createMock(IBuildInfo.class);
+ EasyMock.expect(mMockBuildInfo.getRemoteFiles()).andReturn(null).once();
mMockSysChecker = EasyMock.createMock(ISystemStatusChecker.class);
mMockLogSaver = EasyMock.createMock(ILogSaver.class);
mStubMainConfiguration = new Configuration("stub", "stub");
@@ -778,10 +786,8 @@
mMockListener.testRunStarted(
EasyMock.eq(TEST_CONFIG_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
EasyMock.expectLastCall().times(1);
- mMockListener.testRunFailed(
- "runtime"
- + TestRunResult.ERROR_DIVIDER
- + "Module test only ran 0 out of 1 expected tests.");
+ Capture<String> captured = new Capture<>();
+ mMockListener.testRunFailed(EasyMock.capture(captured));
EasyMock.expect(
mMockDevice.logBugreport(
EasyMock.eq("module-test-failure-SERIAL-bugreport"),
@@ -794,6 +800,12 @@
replayMocks();
mTestSuite.run(mMockListener);
verifyMocks();
+ String exception = captured.getValue();
+ assertTrue(exception.contains("runtime"));
+ assertTrue(
+ exception.contains(
+ TestRunResult.ERROR_DIVIDER
+ + "Module test only ran 0 out of 1 expected tests."));
}
/**
@@ -804,6 +816,7 @@
@Test
public void testShardModules_notShardable() {
mTestSuite = new TestSuiteImpl(5);
+ mTestSuite.setBuild(mMockBuildInfo);
Collection<IRemoteTest> tests = mTestSuite.split(3);
assertEquals(5, tests.size());
for (IRemoteTest test : tests) {
@@ -827,6 +840,7 @@
// default runtime hint is 0, it is only meant to be used for sharding.
assertEquals(0l, mTestSuite.getRuntimeHint());
mTestSuite = new TestSuiteImpl(5);
+ mTestSuite.setBuild(mMockBuildInfo);
Collection<IRemoteTest> tests = mTestSuite.split(3);
for (IRemoteTest test : tests) {
assertTrue(test instanceof TestSuiteImpl);
@@ -1478,7 +1492,9 @@
mTestSuite.setDevice(mMockDevice);
mTestSuite.setBuild(mMockBuildInfo);
mTestSuite.setConfiguration(mStubMainConfiguration);
- mTestSuite.setMaxRunLimit(maxRunLimit);
+ mStubMainConfiguration.getCommandOptions().setMaxRetryCount(maxRunLimit);
+ OptionSetter cmdSetter = new OptionSetter(mStubMainConfiguration.getCommandOptions());
+ cmdSetter.setOptionValue("retry-strategy", "RETRY_ANY_FAILURE");
mContext = new InvocationContext();
mTestSuite.setInvocationContext(mContext);
mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
@@ -1612,6 +1628,8 @@
@Test
public void testRandomizeTestModulesWithSameSeed() throws Exception {
mTestSuite = new TestSuiteImpl(5);
+ mTestSuite.setBuild(mMockBuildInfo);
+
LinkedHashMap<String, IConfiguration> testConfigs = mTestSuite.loadTests();
List<ModuleDefinition> runModules = getRunModules(testConfigs);
List<ModuleDefinition> runModules2 = getRunModules(testConfigs);
@@ -1632,6 +1650,8 @@
@Test
public void testRandomizeTestModulesWithDifferentSeed() throws Exception {
mTestSuite = new TestSuiteImpl(5);
+ mTestSuite.setBuild(mMockBuildInfo);
+
LinkedHashMap<String, IConfiguration> testConfigs = mTestSuite.loadTests();
List<ModuleDefinition> runModules = getRunModules(testConfigs);
List<ModuleDefinition> runModules2 = getRunModules(testConfigs);
@@ -1641,4 +1661,59 @@
assertFalse(runModules.toString().equals(runModules2.toString()));
}
+ /**
+ * Test for {@link ITestSuite#randomizeTestModules(List, long)} to make sure the random-seed
+ * be injected into BuildInfo correctly.
+ */
+ @Test
+ public void testSeedwhenRandomization() throws Exception {
+ IBuildInfo mMockInfo = new BuildInfo();
+ mTestSuite.setBuild(mMockInfo);
+
+ List<ModuleDefinition> runModules = getRunModules(mTestSuite.loadTests());
+ mTestSuite.randomizeTestModules(runModules, 123L);
+
+ String randomSeed = mMockInfo.getBuildAttributes().get(ITestSuite.RANDOM_SEED);
+ assertTrue(randomSeed.equals(String.valueOf(123L)));
+ }
+
+ /**
+ * Test for {@link ITestSuite#stageTestArtifacts(Set)} is called when test zip build artifact
+ * staging is delayed.
+ */
+ @Test
+ public void testStageTestArtifacts() throws Exception {
+ String remoteFilePath = "gs://module1/tests.zip";
+ List<String> files =
+ Arrays.asList("dir/test/test.config", "dir/test/file1", "module2/file1");
+ DynamicRemoteFileResolver dynamicResolver =
+ new DynamicRemoteFileResolver() {
+ @Override
+ public void resolvePartialDownloadZip(
+ File destDir,
+ String remoteFilePath,
+ List<String> includeFilters,
+ List<String> excludeFilters)
+ throws ConfigurationException {
+ assertEquals(new File("tests_dir"), destDir);
+ assertEquals(remoteFilePath, remoteFilePath);
+ assertArrayEquals(new String[] {"/test/"}, includeFilters.toArray());
+ assertArrayEquals(new String[] {"[.]config$"}, excludeFilters.toArray());
+ }
+ };
+ mTestSuite.setDynamicResolver(dynamicResolver);
+ IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+ EasyMock.expect(mockBuildInfo.getTestsDir()).andStubReturn(new File("tests_dir"));
+ EasyMock.expect(mockBuildInfo.getRemoteFiles())
+ .andReturn(new HashSet<File>(Arrays.asList(new File(remoteFilePath))))
+ .times(3);
+ mTestSuite.setBuild(mockBuildInfo);
+
+ List<ISystemStatusChecker> checkers = new ArrayList<ISystemStatusChecker>();
+ mTestSuite.setSystemStatusChecker(checkers);
+
+ EasyMock.replay(mockBuildInfo);
+ mTestSuite.run(mMockListener);
+ EasyMock.verify(mockBuildInfo);
+ }
}
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
index c71fea9..6bb0d10 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleDefinitionTest.java
@@ -57,7 +57,7 @@
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.testtype.ITestFilterReceiver;
-import com.android.tradefed.testtype.suite.ITestSuite.RetryStrategy;
+import com.android.tradefed.testtype.retry.RetryStrategy;
import com.android.tradefed.testtype.suite.module.BaseModuleController;
import com.android.tradefed.testtype.suite.module.IModuleController;
import com.android.tradefed.testtype.suite.module.TestFailureModuleController;
@@ -413,7 +413,11 @@
EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
// Exception thrown during tear down do not bubble up to invocation.
EasyMock.expectLastCall().andThrow(new RuntimeException("teardown failed"));
- mMockListener.testRunStarted(MODULE_NAME, testCount);
+ mMockListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME),
+ EasyMock.eq(testCount),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
for (int i = 0; i < 1; i++) {
mMockListener.testStarted((TestDescription) EasyMock.anyObject(), EasyMock.anyLong());
mMockListener.testEnded(
@@ -557,7 +561,8 @@
.addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
mMockCleaner.tearDown(EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo),
EasyMock.isNull());
- mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+ mMockListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
mMockListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -594,7 +599,8 @@
.addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
mMockCleaner.tearDown(
EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
- mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+ mMockListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
mMockListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -628,7 +634,8 @@
.addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
mMockCleaner.tearDown(
EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
- mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+ mMockListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
mMockListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -668,12 +675,14 @@
.addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
mMockCleaner.tearDown(
EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
- mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+ mMockListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
mMockListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
// Ensure that module listeners receive the callbacks too.
- mockModuleListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+ mockModuleListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
mockModuleListener.testRunFailed(EasyMock.contains(exceptionMessage));
mockModuleListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -710,7 +719,8 @@
.addDeviceBuildInfo(DEFAULT_DEVICE_NAME, mMockBuildInfo);
mMockCleaner.tearDown(
EasyMock.eq(mMockDevice), EasyMock.eq(mMockBuildInfo), EasyMock.isNull());
- mMockListener.testRunStarted(EasyMock.eq(MODULE_NAME), EasyMock.eq(1));
+ mMockListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME), EasyMock.eq(1), EasyMock.eq(0), EasyMock.anyLong());
mMockListener.testRunFailed(EasyMock.contains(exceptionMessage));
mMockListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -892,7 +902,8 @@
// Only one module
assertEquals(1, mModule.getTestsResults().size());
assertEquals(2, mModule.getTestsResults().get(0).getNumCompleteTests());
- assertEquals("assert error", mModule.getTestsResults().get(0).getRunFailureMessage());
+ assertTrue(
+ mModule.getTestsResults().get(0).getRunFailureMessage().contains("assert error"));
verifyMocks();
}
@@ -1104,7 +1115,8 @@
EasyMock.eq(loggedFile));
mMockLogSaverListener.logAssociation("testlogclass", loggedFile);
- mMockLogSaverListener.testRunStarted(MODULE_NAME, 0);
+ mMockLogSaverListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME), EasyMock.eq(0), EasyMock.eq(0), EasyMock.anyLong());
mMockLogSaverListener.testRunEnded(
EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
@@ -1271,7 +1283,11 @@
mMockListener.testRunEnded(
EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
- mMockLogSaverListener.testRunStarted(MODULE_NAME, testCount);
+ mMockLogSaverListener.testRunStarted(
+ EasyMock.eq(MODULE_NAME),
+ EasyMock.eq(testCount),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
for (int i = 0; i < testCount; i++) {
mMockLogSaverListener.testStarted(
(TestDescription) EasyMock.anyObject(), EasyMock.anyLong());
@@ -1379,7 +1395,7 @@
mMockListener.testRunStarted(
EasyMock.eq("fakeName"), EasyMock.eq(0), EasyMock.eq(0), EasyMock.anyLong());
- mMockListener.testRunFailed("early failure!");
+ mMockListener.testRunFailed(EasyMock.contains("early failure!"));
mMockListener.testRunEnded(
EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
diff --git a/tests/src/com/android/tradefed/testtype/suite/ModuleListenerTest.java b/tests/src/com/android/tradefed/testtype/suite/ModuleListenerTest.java
index 1e6a6b4..e00bbe3 100644
--- a/tests/src/com/android/tradefed/testtype/suite/ModuleListenerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/ModuleListenerTest.java
@@ -155,10 +155,10 @@
int finalRunFailureAtAttempt =
Math.max(clearRun1FailureAtAttempt, clearRun2FailureAtAttempt);
for (int attempt = 0; attempt < finalRunFailureAtAttempt; attempt++) {
- assertTrue(mListener.hasRunCrashedAtAttempt(attempt));
+ assertTrue(hasRunCrashed(mListener.getTestRunForAttempts(attempt)));
}
for (int attempt = finalRunFailureAtAttempt; attempt < maxRunLimit; attempt++) {
- assertFalse(mListener.hasRunCrashedAtAttempt(attempt));
+ assertFalse(hasRunCrashed(mListener.getTestRunForAttempts(attempt)));
}
}
@@ -191,4 +191,13 @@
+ "test.apex did not report any run.",
results.get(1).getRunFailureMessage());
}
+
+ private boolean hasRunCrashed(List<TestRunResult> results) {
+ for (TestRunResult run : results) {
+ if (run != null && run.isRunFailure()) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
index 4c7a8ba..b5bbd81 100644
--- a/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/SuiteModuleLoaderTest.java
@@ -56,6 +56,12 @@
+ "$TestInject\" />\n"
+ "</configuration>";
+ private static final String TEST_INSTANT_CONFIG =
+ "<configuration description=\"Runs a stub tests part of some suite\">\n"
+ + " <option name=\"config-descriptor:metadata\" key=\"parameter\" value=\"instant_app\" />"
+ + " <test class=\"com.android.tradefed.testtype.suite.TestSuiteStub\" />\n"
+ + "</configuration>";
+
private SuiteModuleLoader mRepo;
private File mTestsDir;
private Set<IAbi> mAbis;
@@ -83,11 +89,19 @@
FileUtil.writeToFile(TEST_CONFIG, module);
}
+ private void createInstantModuleConfig(String moduleName) throws IOException {
+ File module = new File(mTestsDir, moduleName + SuiteModuleLoader.CONFIG_EXT);
+ FileUtil.writeToFile(TEST_INSTANT_CONFIG, module);
+ }
+
@OptionClass(alias = "test-inject")
public static class TestInject implements IRemoteTest {
@Option(name = "simple-string")
public String test = null;
+ @Option(name = "empty-string")
+ public String testEmpty = null;
+
@Option(name = "alias-option")
public String testAlias = null;
@@ -106,11 +120,14 @@
public void testInjectConfigOptions_moduleArgs() throws Exception {
List<String> moduleArgs = new ArrayList<>();
moduleArgs.add("module1:simple-string:value1");
+ moduleArgs.add("module1:empty-string:"); // value is the empty string
moduleArgs.add("module1:list-string:value2");
moduleArgs.add("module1:list-string:value3");
moduleArgs.add("module1:list-string:set-option:moreoption");
+ moduleArgs.add("module1:list-string:"); // value is the empty string
moduleArgs.add("module1:map-string:set-option:=moreoption");
+ moduleArgs.add("module1:map-string:empty-option:="); // value is the empty string
createModuleConfig("module1");
@@ -131,14 +148,17 @@
TestInject checker = (TestInject) config.getTests().get(0);
assertEquals("value1", checker.test);
+ assertEquals("", checker.testEmpty);
// Check list
- assertTrue(checker.testList.size() == 3);
+ assertTrue(checker.testList.size() == 4);
assertTrue(checker.testList.contains("value2"));
assertTrue(checker.testList.contains("value3"));
assertTrue(checker.testList.contains("set-option:moreoption"));
+ assertTrue(checker.testList.contains(""));
// Chech map
- assertTrue(checker.testMap.size() == 1);
+ assertTrue(checker.testMap.size() == 2);
assertEquals("moreoption", checker.testMap.get("set-option"));
+ assertEquals("", checker.testMap.get("empty-option"));
}
/** Test an end-to-end injection of --test-arg. */
@@ -150,6 +170,9 @@
+ "simple-string:value1");
testArgs.add(
"com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
+ + "empty-string:"); // value is the empty string
+ testArgs.add(
+ "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
+ "list-string:value2");
testArgs.add(
"com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
@@ -159,7 +182,13 @@
+ "list-string:set-option:moreoption");
testArgs.add(
"com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
+ + "list-string:"); // value is the empty string
+ testArgs.add(
+ "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
+ "map-string:set-option:=moreoption");
+ testArgs.add(
+ "com.android.tradefed.testtype.suite.SuiteModuleLoaderTest$TestInject:"
+ + "map-string:empty-option:="); // value is the empty string
createModuleConfig("module1");
@@ -180,14 +209,17 @@
TestInject checker = (TestInject) config.getTests().get(0);
assertEquals("value1", checker.test);
+ assertEquals("", checker.testEmpty);
// Check list
- assertTrue(checker.testList.size() == 3);
+ assertTrue(checker.testList.size() == 4);
assertTrue(checker.testList.contains("value2"));
assertTrue(checker.testList.contains("value3"));
assertTrue(checker.testList.contains("set-option:moreoption"));
+ assertTrue(checker.testList.contains(""));
// Chech map
- assertTrue(checker.testMap.size() == 1);
+ assertTrue(checker.testMap.size() == 2);
assertEquals("moreoption", checker.testMap.get("set-option"));
+ assertEquals("", checker.testMap.get("empty-option"));
}
@Test
@@ -215,4 +247,44 @@
TestInject checker = (TestInject) config.getTests().get(0);
assertEquals("value1", checker.testAlias);
}
+
+ /**
+ * Test that if the base module is excluded in full, the filters of parameterized modules are
+ * still populated with the proper filters.
+ */
+ @Test
+ public void testFilterParameterized() throws Exception {
+ Map<String, List<SuiteTestFilter>> excludeFilters = new LinkedHashMap<>();
+ createInstantModuleConfig("basemodule");
+ SuiteTestFilter fullFilter = SuiteTestFilter.createFrom("armeabi-v7a basemodule");
+ excludeFilters.put("armeabi-v7a basemodule", Arrays.asList(fullFilter));
+
+ SuiteTestFilter instantMethodFilter =
+ SuiteTestFilter.createFrom(
+ "armeabi-v7a basemodule[instant] NativeDnsAsyncTest#Async_Cancel");
+ excludeFilters.put("armeabi-v7a basemodule[instant]", Arrays.asList(instantMethodFilter));
+
+ mRepo =
+ new SuiteModuleLoader(
+ new LinkedHashMap<String, List<SuiteTestFilter>>(),
+ excludeFilters,
+ new ArrayList<>(),
+ new ArrayList<>());
+ mRepo.setParameterizedModules(true);
+
+ List<String> patterns = new ArrayList<>();
+ patterns.add(".*.config");
+ patterns.add(".*.xml");
+ LinkedHashMap<String, IConfiguration> res =
+ mRepo.loadConfigsFromDirectory(
+ Arrays.asList(mTestsDir), mAbis, null, null, patterns);
+ assertEquals(1, res.size());
+ // Full module was excluded completely
+ IConfiguration instantModule = res.get("armeabi-v7a basemodule[instant]");
+ assertNotNull(instantModule);
+ TestSuiteStub stubTest = (TestSuiteStub) instantModule.getTests().get(0);
+ assertEquals(1, stubTest.getExcludeFilters().size());
+ assertEquals(
+ "NativeDnsAsyncTest#Async_Cancel", stubTest.getExcludeFilters().iterator().next());
+ }
}
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
index bbf22c4..cf4eba5 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestMappingSuiteRunnerTest.java
@@ -418,6 +418,7 @@
.andReturn(null);
EasyMock.expect(mockBuildInfo.getTestsDir()).andReturn(new File("non-existing-dir"));
EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+ EasyMock.expect(mockBuildInfo.getRemoteFiles()).andReturn(null).once();
mRunner.setBuild(mockBuildInfo);
EasyMock.replay(mockBuildInfo);
@@ -478,4 +479,111 @@
assertTrue(configMap.containsKey(ABI_2 + " suite/stubAbi"));
EasyMock.verify(mockDevice);
}
+
+ /**
+ * Test for {@link TestMappingSuiteRunner#loadTests()} that when force-test-mapping-module is
+ * specified, tests would be filtered.
+ */
+ @Test
+ public void testLoadTestsWithModule() throws Exception {
+ File tempDir = null;
+ try {
+ OptionSetter setter = new OptionSetter(mRunner);
+ setter.setOptionValue("test-mapping-test-group", "postsubmit");
+ setter.setOptionValue("force-test-mapping-module", "suite/stub1");
+
+ tempDir = FileUtil.createTempDir("test_mapping");
+
+ File srcDir = FileUtil.createTempDir("src", tempDir);
+ String srcFile =
+ File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+ InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, srcDir, DISABLED_PRESUBMIT_TESTS);
+
+ srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+ resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+ File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+ srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+ resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+ List<File> filesToZip =
+ Arrays.asList(srcDir, new File(tempDir, DISABLED_PRESUBMIT_TESTS));
+ File zipFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPINGS_ZIP).toFile();
+ ZipUtil.createZip(filesToZip, zipFile);
+
+ IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+ EasyMock.expect(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR))
+ .andReturn(null);
+ EasyMock.expect(mockBuildInfo.getTestsDir()).andReturn(new File("non-existing-dir"));
+ EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+
+ mRunner.setBuild(mockBuildInfo);
+ EasyMock.replay(mockBuildInfo);
+
+ LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+ assertEquals(2, configMap.size());
+ assertTrue(configMap.containsKey(ABI_1 + " suite/stub1"));
+ assertTrue(configMap.containsKey(ABI_2 + " suite/stub1"));
+ EasyMock.verify(mockBuildInfo);
+ } finally {
+ FileUtil.recursiveDelete(tempDir);
+ }
+ }
+
+ /**
+ * Test for {@link TestMappingSuiteRunner#loadTests()} that when multi force-test-mapping-module
+ * are specified, tests would be filtered.
+ */
+ @Test
+ public void testLoadTestsWithMultiModules() throws Exception {
+ File tempDir = null;
+ try {
+ OptionSetter setter = new OptionSetter(mRunner);
+ setter.setOptionValue("test-mapping-test-group", "postsubmit");
+ setter.setOptionValue("force-test-mapping-module", "suite/stub1");
+ setter.setOptionValue("force-test-mapping-module", "suite/stub2");
+
+ tempDir = FileUtil.createTempDir("test_mapping");
+
+ File srcDir = FileUtil.createTempDir("src", tempDir);
+ String srcFile =
+ File.separator + TEST_DATA_DIR + File.separator + DISABLED_PRESUBMIT_TESTS;
+ InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, srcDir, DISABLED_PRESUBMIT_TESTS);
+
+ srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_1";
+ resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+ File subDir = FileUtil.createTempDir("sub_dir", srcDir);
+ srcFile = File.separator + TEST_DATA_DIR + File.separator + "test_mapping_2";
+ resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, subDir, TEST_MAPPING);
+
+ List<File> filesToZip =
+ Arrays.asList(srcDir, new File(tempDir, DISABLED_PRESUBMIT_TESTS));
+ File zipFile = Paths.get(tempDir.getAbsolutePath(), TEST_MAPPINGS_ZIP).toFile();
+ ZipUtil.createZip(filesToZip, zipFile);
+
+ IDeviceBuildInfo mockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
+ EasyMock.expect(mockBuildInfo.getFile(BuildInfoFileKey.TARGET_LINKED_DIR))
+ .andReturn(null);
+ EasyMock.expect(mockBuildInfo.getTestsDir()).andReturn(new File("non-existing-dir"));
+ EasyMock.expect(mockBuildInfo.getFile(TEST_MAPPINGS_ZIP)).andReturn(zipFile);
+
+ mRunner.setBuild(mockBuildInfo);
+ EasyMock.replay(mockBuildInfo);
+
+ LinkedHashMap<String, IConfiguration> configMap = mRunner.loadTests();
+ assertEquals(4, configMap.size());
+ assertTrue(configMap.containsKey(ABI_1 + " suite/stub1"));
+ assertTrue(configMap.containsKey(ABI_1 + " suite/stub2"));
+ assertTrue(configMap.containsKey(ABI_2 + " suite/stub1"));
+ assertTrue(configMap.containsKey(ABI_2 + " suite/stub2"));
+ EasyMock.verify(mockBuildInfo);
+ } finally {
+ FileUtil.recursiveDelete(tempDir);
+ }
+ }
}
diff --git a/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java b/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
index 58a3e03..4e7aac2 100644
--- a/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
+++ b/tests/src/com/android/tradefed/testtype/suite/TestSuiteStub.java
@@ -86,6 +86,8 @@
description = "The notAnnotation class name of the test name to run, can be repeated")
private Set<String> mExcludeAnnotationFilter = new HashSet<>();
+ private Set<String> mExcludeFilters = new HashSet<>();
+
/** Tests attempt. */
private void testAttempt(ITestInvocationListener listener) throws DeviceNotAvailableException {
listener.testRunStarted(mModule, 3);
@@ -224,10 +226,14 @@
public void addAllIncludeFilters(Set<String> filters) {}
@Override
- public void addExcludeFilter(String filter) {}
+ public void addExcludeFilter(String filter) {
+ mExcludeFilters.add(filter);
+ }
@Override
- public void addAllExcludeFilters(Set<String> filters) {}
+ public void addAllExcludeFilters(Set<String> filters) {
+ mExcludeFilters.addAll(filters);
+ }
@Override
public void clearIncludeFilters() {}
@@ -239,11 +245,13 @@
@Override
public Set<String> getExcludeFilters() {
- return new HashSet<>();
+ return mExcludeFilters;
}
@Override
- public void clearExcludeFilters() {}
+ public void clearExcludeFilters() {
+ mExcludeFilters.clear();
+ }
@Override
public void addIncludeAnnotation(String annotation) {
diff --git a/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java b/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
index ce47dc8..06c8b32 100644
--- a/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/retry/ResultsPlayerTest.java
@@ -16,12 +16,15 @@
package com.android.tradefed.testtype.suite.retry;
import com.android.ddmlib.IDevice;
+import com.android.ddmlib.Log.LogLevel;
import com.android.ddmlib.testrunner.TestResult.TestStatus;
import com.android.tradefed.config.ConfigurationDef;
+import com.android.tradefed.config.IConfiguration;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.log.ILeveledLogOutput;
import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.TestDescription;
@@ -46,6 +49,8 @@
private IInvocationContext mContext;
private ITestDevice mMockDevice;
private IDevice mMockIDevice;
+ private IConfiguration mMockConfig;
+ private ILeveledLogOutput mMockLogOutput;
@Before
public void setUp() throws Exception {
@@ -53,8 +58,16 @@
mMockListener = EasyMock.createStrictMock(ITestInvocationListener.class);
mMockDevice = EasyMock.createMock(ITestDevice.class);
mMockIDevice = EasyMock.createMock(IDevice.class);
+ mMockConfig = EasyMock.createMock(IConfiguration.class);
+ mMockLogOutput = EasyMock.createMock(ILeveledLogOutput.class);
+ EasyMock.expect(mMockConfig.getLogOutput()).andStubReturn(mMockLogOutput);
+ EasyMock.expect(mMockLogOutput.getLogLevel()).andReturn(LogLevel.VERBOSE);
+ mMockLogOutput.setLogLevel(LogLevel.WARN);
+ mMockLogOutput.setLogLevel(LogLevel.VERBOSE);
+
mPlayer = new ResultsPlayer();
mPlayer.setInvocationContext(mContext);
+ mPlayer.setConfiguration(mMockConfig);
mContext.addAllocatedDevice(ConfigurationDef.DEFAULT_DEVICE_NAME, mMockDevice);
EasyMock.expect(mMockDevice.getIDevice()).andReturn(mMockIDevice);
@@ -82,9 +95,9 @@
EasyMock.eq(new HashMap<String, Metric>()));
mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
- EasyMock.replay(mMockListener, mMockDevice);
+ EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
mPlayer.run(mMockListener);
- EasyMock.verify(mMockListener, mMockDevice);
+ EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
}
/** Test that when replaying a module we properly replay all the results. */
@@ -128,9 +141,9 @@
mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
mMockListener.testModuleEnded();
- EasyMock.replay(mMockListener, mMockDevice);
+ EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
mPlayer.run(mMockListener);
- EasyMock.verify(mMockListener, mMockDevice);
+ EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
}
/** Test that the replay of a single requested test case is working. */
@@ -155,9 +168,9 @@
EasyMock.eq(test), EasyMock.anyLong(), EasyMock.eq(new HashMap<String, Metric>()));
mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
- EasyMock.replay(mMockListener, mMockDevice);
+ EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
mPlayer.run(mMockListener);
- EasyMock.verify(mMockListener, mMockDevice);
+ EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
}
/** Test requesting several tests to re-run. */
@@ -198,9 +211,9 @@
mMockListener.testRunEnded(500L, new HashMap<String, Metric>());
- EasyMock.replay(mMockListener, mMockDevice);
+ EasyMock.replay(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
mPlayer.run(mMockListener);
- EasyMock.verify(mMockListener, mMockDevice);
+ EasyMock.verify(mMockListener, mMockDevice, mMockConfig, mMockLogOutput);
}
private TestRunResult createTestRunResult(String runName, int testCount, int failCount) {
diff --git a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
index 23d219c..380e75c 100644
--- a/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
+++ b/tests/src/com/android/tradefed/testtype/suite/retry/RetryReschedulerTest.java
@@ -35,6 +35,8 @@
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.result.proto.ProtoResultReporter;
import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
+import com.android.tradefed.testtype.Abi;
+import com.android.tradefed.testtype.IAbi;
import com.android.tradefed.testtype.suite.BaseTestSuite;
import org.easymock.EasyMock;
@@ -345,6 +347,50 @@
verify(mSuite).setExcludeFilter(excludeRun1);
}
+ /**
+ * Test that if an exclude-filter is provided without abi, we are still able to exclude all the
+ * matching modules for all abis.
+ */
+ @Test
+ public void testReschedule_excludeFilters_abi() throws Exception {
+ OptionSetter setter = new OptionSetter(mTest);
+ // We specify to exclude "run1"
+ setter.setOptionValue(BaseTestSuite.EXCLUDE_FILTER_OPTION, "run1");
+ populateFakeResults(2, 2, 1, 0, 0, false, new Abi("armeabi-v7a", "32"));
+ mMockLoader.init();
+ EasyMock.expect(mMockLoader.getCommandLine()).andReturn("previous_command");
+ EasyMock.expect(mMockFactory.createConfigurationFromArgs(EasyMock.anyObject()))
+ .andReturn(mRescheduledConfiguration);
+ EasyMock.expect(mMockLoader.loadPreviousRecord()).andReturn(mFakeRecord);
+
+ mRescheduledConfiguration.setTests(EasyMock.anyObject());
+ EasyMock.expectLastCall().times(1);
+
+ EasyMock.expect(mMockRescheduler.scheduleConfig(mRescheduledConfiguration)).andReturn(true);
+ EasyMock.replay(
+ mMockRescheduler,
+ mMockLoader,
+ mMockFactory,
+ mRescheduledConfiguration,
+ mMockCommandOptions);
+ mTest.run(null);
+ EasyMock.verify(
+ mMockRescheduler,
+ mMockLoader,
+ mMockFactory,
+ mRescheduledConfiguration,
+ mMockCommandOptions);
+
+ Set<String> excludeRun0 = new HashSet<>();
+ // Run with the abi are excluded
+ excludeRun0.add("armeabi-v7a run0 test.class#testPass0");
+ verify(mSuite).setExcludeFilter(excludeRun0);
+ Set<String> excludeRun1 = new HashSet<>();
+ // Even if run1 had failed test cases, it was excluded so it's not running.
+ excludeRun1.add("armeabi-v7a run1");
+ verify(mSuite).setExcludeFilter(excludeRun1);
+ }
+
/** Test rescheduling a configuration when no parameterized tests previously failed. */
@Test
public void testReschedule_parameterized_nofail() throws Exception {
@@ -452,6 +498,18 @@
int assumpFailure,
int parameterized,
boolean failedParam) {
+ populateFakeResults(
+ numModule, numTests, failedTests, assumpFailure, parameterized, failedParam, null);
+ }
+
+ private void populateFakeResults(
+ int numModule,
+ int numTests,
+ int failedTests,
+ int assumpFailure,
+ int parameterized,
+ boolean failedParam,
+ IAbi abi) {
ProtoResultReporter reporter =
new ProtoResultReporter() {
@Override
@@ -464,7 +522,11 @@
context.addDeviceBuildInfo(ConfigurationDef.DEFAULT_DEVICE_NAME, new BuildInfo());
reporter.invocationStarted(context);
for (int i = 0; i < numModule; i++) {
- reporter.testRunStarted("run" + i, numTests);
+ String runName = "run" + i;
+ if (abi != null) {
+ runName = abi.getName() + " " + runName;
+ }
+ reporter.testRunStarted(runName, numTests);
for (int j = 0; j < numTests - failedTests - assumpFailure - parameterized; j++) {
TestDescription test = new TestDescription("test.class", "testPass" + j);
reporter.testStarted(test);
diff --git a/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsParserTest.java b/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsParserTest.java
deleted file mode 100644
index 61c74bd..0000000
--- a/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsParserTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-import com.android.tradefed.util.xml.AbstractXmlParser.ParseException;
-
-import junit.framework.TestCase;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-
-/**
- * Unit tests for {@link XmlDefsParser}.
- */
-public class XmlDefsParserTest extends TestCase {
-
- static final String TEST_PKG = "testpkg";
- static final String TEST_COVERAGE_TARGET = "testcoverage";
-
- private static final String TEST_DATA =
- "<test name=\"frameworks-core\" " +
- "package=\"" + TEST_PKG + "\" " +
- "continuous=\"true\" />";
-
- private static final String NON_CONTINUOUS_TEST_DATA =
- "<test name=\"frameworks-core\" " +
- "package=\"" + TEST_PKG + "\" " +
- "/>";
-
- private static final String FALSE_CONTINUOUS_TEST_DATA =
- "<test name=\"frameworks-core\" " +
- "package=\"" + TEST_PKG + "\" " +
- "continuous=\"false\" />";
-
- static final String FULL_DATA =
- "<test name=\"frameworks-core\" " +
- "package=\"" + TEST_PKG + "\" " +
- "class=\"class\" " +
- "runner=\"runner\" " +
- "continuous=\"true\" " +
- "coverage_target=\"" + TEST_COVERAGE_TARGET + "\" />";
-
- /**
- * Simple test for parsing a single test definition.
- */
- public void testParseSingleDef() throws ParseException {
- XmlDefsParser parser = new XmlDefsParser();
- parser.parse(getStringAsStream(TEST_DATA));
- assertEquals(1, parser.getTestDefs().size());
- InstrumentationTestDef def = parser.getTestDefs().iterator().next();
- assertEquals("frameworks-core", def.getName());
- assertEquals(TEST_PKG, def.getPackage());
- assertTrue(def.isContinuous());
- assertNull(def.getClassName());
- assertNull(def.getRunner());
- assertNull(def.getCoverageTarget());
- }
-
- /**
- * Test for parsing a test definition without continuous attribute.
- */
- public void testParseNonContinuous() throws ParseException {
- XmlDefsParser parser = new XmlDefsParser();
- parser.parse(getStringAsStream(NON_CONTINUOUS_TEST_DATA));
- assertEquals(1, parser.getTestDefs().size());
- InstrumentationTestDef def = parser.getTestDefs().iterator().next();
- assertFalse(def.isContinuous());
- }
-
- /**
- * Test for parsing a test definition with continuous attribute equal false.
- */
- public void testParseFaleContinuous() throws ParseException {
- XmlDefsParser parser = new XmlDefsParser();
- parser.parse(getStringAsStream(FALSE_CONTINUOUS_TEST_DATA));
- assertEquals(1, parser.getTestDefs().size());
- InstrumentationTestDef def = parser.getTestDefs().iterator().next();
- assertFalse(def.isContinuous());
- }
-
- /**
- * Simple test for parsing a single test definition with all attributes defined.
- */
- public void testParseFullDef() throws ParseException {
- XmlDefsParser parser = new XmlDefsParser();
- parser.parse(getStringAsStream(FULL_DATA));
- assertEquals(1, parser.getTestDefs().size());
- InstrumentationTestDef def = parser.getTestDefs().iterator().next();
- assertEquals("frameworks-core", def.getName());
- assertEquals(TEST_PKG, def.getPackage());
- assertTrue(def.isContinuous());
- assertEquals("class", def.getClassName());
- assertEquals("runner", def.getRunner());
- assertEquals(TEST_COVERAGE_TARGET, def.getCoverageTarget());
- }
-
- /**
- * Test parsing non-xml data throws a ParseException
- */
- public void testParseException() {
- try {
- XmlDefsParser parser = new XmlDefsParser();
- parser.parse(getStringAsStream("ghgh"));
- fail("ParseException not thrown");
- } catch (ParseException e) {
- // expected
- }
- }
-
- private InputStream getStringAsStream(String input) {
- return new ByteArrayInputStream(input.getBytes());
- }
-}
diff --git a/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsTestTest.java b/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsTestTest.java
deleted file mode 100644
index 03466f6..0000000
--- a/tests/src/com/android/tradefed/testtype/testdefs/XmlDefsTestTest.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.testdefs;
-
-import com.android.tradefed.device.DeviceNotAvailableException;
-import com.android.tradefed.device.ITestDevice;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.testtype.InstrumentationTest;
-import com.android.tradefed.testtype.MockInstrumentationTest;
-
-import junit.framework.TestCase;
-
-import org.easymock.Capture;
-import org.easymock.EasyMock;
-import org.easymock.IAnswer;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.HashMap;
-
-/**
- * Unit tests for {@link XmlDefsTest}.
- */
-public class XmlDefsTestTest extends TestCase {
-
- private static final String TEST_PATH = "foopath";
- private static final String TEST_DEF_DATA = XmlDefsParserTest.FULL_DATA;
- private static final String TEST_PKG = XmlDefsParserTest.TEST_PKG;
- private static final String TEST_COVERAGE_TARGET = XmlDefsParserTest.TEST_COVERAGE_TARGET;
- private ITestDevice mMockTestDevice;
- private ITestInvocationListener mMockListener;
- private XmlDefsTest mXmlTest;
- private MockInstrumentationTest mMockInstrumentationTest;
-
- /**
- * {@inheritDoc}
- */
- @Override
- protected void setUp() throws Exception {
- super.setUp();
-
- mMockTestDevice = EasyMock.createMock(ITestDevice.class);
- EasyMock.expect(mMockTestDevice.getSerialNumber()).andReturn("foo").anyTimes();
- mMockListener = EasyMock.createMock(ITestInvocationListener.class);
- mMockInstrumentationTest = new MockInstrumentationTest();
-
- mXmlTest = new XmlDefsTest() {
- @Override
- InstrumentationTest createInstrumentationTest() {
- return mMockInstrumentationTest;
- }
- };
- mXmlTest.setDevice(mMockTestDevice);
- }
-
- /**
- * Test the run normal case. Simple verification that expected data is passed along, etc.
- */
- public void testRun() throws DeviceNotAvailableException {
- mXmlTest.addRemoteFilePath(TEST_PATH);
-
- injectMockXmlData();
- mMockListener.testRunStarted(TEST_PKG, 0);
- Capture<HashMap<String, Metric>> captureMetrics = new Capture<HashMap<String, Metric>>();
- mMockListener.testRunEnded(EasyMock.anyLong(), EasyMock.capture(captureMetrics));
- EasyMock.replay(mMockTestDevice, mMockListener);
- mXmlTest.run(mMockListener);
- assertEquals(mMockListener, mMockInstrumentationTest.getListener());
- assertEquals(TEST_PKG, mMockInstrumentationTest.getPackageName());
- assertEquals(
- TEST_COVERAGE_TARGET,
- captureMetrics
- .getValue()
- .get(XmlDefsTest.COVERAGE_TARGET_KEY)
- .getMeasurements()
- .getSingleString());
- }
-
- private void injectMockXmlData() throws DeviceNotAvailableException {
- // TODO: it would be nice to mock out the file objects, so this test wouldn't need to do
- // IO
- mMockTestDevice.pullFile(EasyMock.eq(TEST_PATH), (File)EasyMock.anyObject());
- EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
- @Override
- public Object answer() throws Throwable {
- // simulate the pull file by dumping data into local file
- FileOutputStream outStream;
- try {
- outStream = new FileOutputStream((File)EasyMock.getCurrentArguments()[1]);
- outStream.write(TEST_DEF_DATA.getBytes());
- outStream.close();
- return true;
- } catch (IOException e) {
- fail(e.toString());
- }
- return false;
- }
- });
- }
-
- /**
- * Test a run that was aborted then resumed
- */
- public void testRun_resume() throws DeviceNotAvailableException {
- mXmlTest.addRemoteFilePath(TEST_PATH);
- // turn off sending of coverage for simplicity
- mXmlTest.setSendCoverage(false);
- injectMockXmlData();
- mMockInstrumentationTest.setException(new DeviceNotAvailableException());
- EasyMock.replay(mMockTestDevice, mMockListener);
- try {
- mXmlTest.run(mMockListener);
- fail("DeviceNotAvailableException not thrown");
- } catch (DeviceNotAvailableException e) {
- // expected
- }
- // verify InstrumentationTest.run was called
- assertEquals(mMockListener, mMockInstrumentationTest.getListener());
- mMockInstrumentationTest.setException(null);
- mMockInstrumentationTest.clearListener();
- // resume test run, on a different device
- ITestDevice newTestDevice = EasyMock.createMock(ITestDevice.class);
- mXmlTest.setDevice(newTestDevice);
- mXmlTest.run(mMockListener);
- // verify InstrumentationTest.run was called again, with same listener + different device
- assertEquals(mMockListener, mMockInstrumentationTest.getListener());
- assertEquals(newTestDevice, mMockInstrumentationTest.getDevice());
- }
-
- /**
- * Test that IllegalArgumentException is thrown when attempting run without setting device.
- */
- public void testRun_noDevice() throws Exception {
- mXmlTest.addRemoteFilePath(TEST_PATH);
- mXmlTest.setDevice(null);
- try {
- mXmlTest.run(mMockListener);
- fail("IllegalArgumentException not thrown");
- } catch (IllegalArgumentException e) {
- // expected
- }
- assertNull(mMockInstrumentationTest.getPackageName());
- }
-
- /**
- * Test that IllegalArgumentException is thrown when attempting run without setting any file
- * paths.
- */
- public void testRun_noPath() throws Exception {
- try {
- mXmlTest.run(mMockListener);
- fail("IllegalArgumentException not thrown");
- } catch (IllegalArgumentException e) {
- // expected
- }
- assertNull(mMockInstrumentationTest.getPackageName());
- }
-}
diff --git a/tests/src/com/android/tradefed/util/BundletoolUtilTest.java b/tests/src/com/android/tradefed/util/BundletoolUtilTest.java
index e288bca..c61cc46 100644
--- a/tests/src/com/android/tradefed/util/BundletoolUtilTest.java
+++ b/tests/src/com/android/tradefed/util/BundletoolUtilTest.java
@@ -23,6 +23,8 @@
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.ITestDevice;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Before;
@@ -61,6 +63,11 @@
protected String getAdbPath() {
return "adb";
}
+
+ @Override
+ protected File getBundletoolFile() {
+ return mBundletoolJar;
+ }
};
}
@@ -88,12 +95,17 @@
(String) EasyMock.anyObject(),
(String) EasyMock.anyObject(),
(String) EasyMock.anyObject(),
+ (String) EasyMock.anyObject(),
(String) EasyMock.anyObject()))
.andReturn(res)
.once();
+ Path expectedSpecFilePath =
+ Paths.get(mBundletoolJar.getParentFile().getAbsolutePath(), "serial.json");
+
EasyMock.replay(mMockDevice, mMockRuntil);
- mBundletoolUtil.generateDeviceSpecFile(mMockDevice);
+ String actualSpecFilePath = mBundletoolUtil.generateDeviceSpecFile(mMockDevice);
+ assertEquals(expectedSpecFilePath.toString(), actualSpecFilePath);
EasyMock.verify(mMockRuntil);
EasyMock.verify(mMockDevice);
}
diff --git a/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java b/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
index 01662a1..4a9d110 100644
--- a/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
+++ b/tests/src/com/android/tradefed/util/LocalRunInstructionBuilderTest.java
@@ -17,9 +17,9 @@
import static org.junit.Assert.assertEquals;
-import com.android.tradefed.config.ConfigurationDef.OptionDef;
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.ConfigurationDescriptor.LocalTestRunner;
+import com.android.tradefed.config.OptionDef;
import com.android.tradefed.result.TestDescription;
import com.android.tradefed.testtype.Abi;
@@ -103,4 +103,20 @@
+ "atest module_name:class_name -- --abi arm",
instruction);
}
+
+ /** Test when a parameterized module needs to be repro. */
+ @Test
+ public void testGetInstruction_withParameter() {
+ ConfigurationDescriptor configDescriptor = new ConfigurationDescriptor();
+ configDescriptor.setAbi(new Abi(ABI_NAME, "32"));
+ configDescriptor.setModuleName(OPTION_SOURCE);
+ configDescriptor.addMetadata(ConfigurationDescriptor.PARAMETER_KEY, "instant");
+ String instruction =
+ LocalRunInstructionBuilder.getInstruction(
+ configDescriptor, LocalTestRunner.ATEST, null);
+ assertEquals(
+ "Run following command to try the test in a local setup:\n"
+ + "atest module_name -- --abi arm --instant",
+ instruction);
+ }
}
diff --git a/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java b/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java
new file mode 100644
index 0000000..523655c
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/NativeCodeCoverageFlusherTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.util;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.IllegalStateException;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class NativeCodeCoverageFlusherTest {
+
+ @Mock ITestDevice mMockDevice;
+
+ // Object under test
+ NativeCodeCoverageFlusher mFlusher;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mFlusher = new NativeCodeCoverageFlusher(mMockDevice);
+ }
+
+ @Test
+ public void testClearCoverageMeasurements_rmCommandCalled() throws DeviceNotAvailableException {
+ doReturn(true).when(mMockDevice).isAdbRoot();
+
+ mFlusher.clearCoverageMeasurements();
+
+ // Verify that the rm command was executed.
+ verify(mMockDevice).executeShellCommand("rm -rf /data/misc/trace/*");
+ }
+
+ @Test
+ public void testNoAdbRootClearCoverageMeasurements_noOp() throws DeviceNotAvailableException {
+ doReturn(false).when(mMockDevice).isAdbRoot();
+
+ try {
+ mFlusher.clearCoverageMeasurements();
+ fail("Should have thrown an exception");
+ } catch (IllegalStateException e) {
+ // Expected
+ }
+
+ // Verify that no shell command was executed.
+ verify(mMockDevice, never()).executeShellCommand(anyString());
+ }
+
+ @Test
+ public void testFlushCoverageAllProcesses_flushAllCommandCalled()
+ throws DeviceNotAvailableException {
+ doReturn(true).when(mMockDevice).isAdbRoot();
+
+ mFlusher.forceCoverageFlush(ImmutableList.of());
+
+ // Verify that the flush command for all processes was called.
+ verify(mMockDevice).executeShellCommand("kill -37 -1");
+ }
+
+ @Test
+ public void testFlushCoverageSpecificProcesses_flushSpecificCommandCalled()
+ throws DeviceNotAvailableException {
+ List<String> processes = ImmutableList.of("mediaserver", "mediaextractor");
+
+ doReturn(true).when(mMockDevice).isAdbRoot();
+ doReturn("12").when(mMockDevice).getProcessPid(processes.get(0));
+ doReturn("789").when(mMockDevice).getProcessPid(processes.get(1));
+
+ mFlusher.forceCoverageFlush(processes);
+
+ // Verify that the flush command for the specific processes was called.
+ verify(mMockDevice).executeShellCommand("kill -37 12 789");
+ }
+
+ @Test
+ public void testNoAdbRootFlush_noOp() throws DeviceNotAvailableException {
+ doReturn(false).when(mMockDevice).isAdbRoot();
+
+ try {
+ mFlusher.forceCoverageFlush(ImmutableList.of("mediaserver"));
+ fail("Should have thrown an exception");
+ } catch (IllegalStateException e) {
+ // Expected
+ }
+
+ // Verify no shell commands or pid lookups were executed.
+ verify(mMockDevice, never()).executeShellCommand(anyString());
+ verify(mMockDevice, never()).getProcessPid(anyString());
+ }
+}
diff --git a/tests/src/com/android/tradefed/util/StreamUtilTest.java b/tests/src/com/android/tradefed/util/StreamUtilTest.java
index f1a238c..f2e774c 100644
--- a/tests/src/com/android/tradefed/util/StreamUtilTest.java
+++ b/tests/src/com/android/tradefed/util/StreamUtilTest.java
@@ -23,8 +23,10 @@
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
@@ -119,6 +121,20 @@
}
/**
+ * Verify that {@link com.android.tradefed.util.StreamUtil#calculateCrc32(InputStream)} works as
+ * expected.
+ *
+ * @throws IOException
+ */
+ public void testCalculateCrc32() throws IOException {
+ final String source = getLargeText();
+ final long crc32 = 3023941728L;
+ ByteArrayInputStream inputSource = new ByteArrayInputStream(source.getBytes());
+ long actualCrc32 = StreamUtil.calculateCrc32(inputSource);
+ assertEquals(crc32, actualCrc32);
+ }
+
+ /**
* Verify that {@link com.android.tradefed.util.StreamUtil#calculateMd5(InputStream)} works as
* expected.
*
@@ -156,6 +172,46 @@
assertEquals(text, baos.toString());
}
+ /**
+ * Verify that {@link com.android.tradefed.util.StreamUtil#copyStreams(InputStream,
+ * OutputStream, int, int)} can copy partial content.
+ */
+ public void testCopyStreams_partialSuccess() throws Exception {
+ String text = getLargeText();
+ StringBuilder builder = new StringBuilder(33 * 1024);
+ // Create a string longer than StreamUtil.BUF_SIZE
+ while (builder.length() < 32 * 1024) {
+ builder.append(text);
+ }
+ ByteArrayInputStream bais = new ByteArrayInputStream(builder.toString().getBytes());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ // Skip the first 1kB, and read longer than StreamUtil.BUF_SIZE
+ StreamUtil.copyStreams(bais, baos, 1024, 20 * 1024);
+ bais.close();
+ baos.close();
+ assertEquals(builder.toString().substring(1024, 21 * 1024), baos.toString());
+ }
+
+ /**
+ * Verify that {@link com.android.tradefed.util.StreamUtil#copyStreams(InputStream,
+ * OutputStream, int, int)} cannot copy partial content if requested size is larger than what's
+ * available.
+ */
+ public void testCopyStreams_partialFail() throws Exception {
+ ByteArrayInputStream bais = null;
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ String text = getLargeText();
+ bais = new ByteArrayInputStream(text.getBytes());
+ // Skip the first 1kB, and read longer than the size of text
+ StreamUtil.copyStreams(bais, baos, 10, text.length() + 1024);
+ fail("IOException should be thrown when reading too much data.");
+ } catch (IOException e) {
+ // Ignore expected error.
+ } finally {
+ StreamUtil.close(bais);
+ }
+ }
+
public void testCopyStreamToWriter() throws Exception {
String text = getLargeText();
ByteArrayInputStream bais = new ByteArrayInputStream(text.getBytes());
@@ -169,6 +225,26 @@
}
/**
+ * Verify that {@link com.android.tradefed.util.StreamUtil#copyFileToStream(File, OutputStream)}
+ * works as expected.
+ *
+ * @throws IOException
+ */
+ public void testCopyFileToStream() throws IOException {
+ String text = getLargeText();
+ File file = File.createTempFile("testCopyFileToStream", ".txt");
+ try {
+ FileUtil.writeToFile(text, file);
+ try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
+ StreamUtil.copyFileToStream(file, outStream);
+ assertEquals(text, outStream.toString());
+ }
+ } finally {
+ file.delete();
+ }
+ }
+
+ /**
* Returns a large chunk of text that's at least 16K in size
*/
private String getLargeText() {
diff --git a/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java b/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
index ffec8e4..9d84bab 100644
--- a/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
+++ b/tests/src/com/android/tradefed/util/SubprocessTestResultsParserTest.java
@@ -89,16 +89,20 @@
String[] contents = readInFile(SUBPROC_OUTPUT_FILE_1);
ITestInvocationListener mockRunListener =
EasyMock.createMock(ITestInvocationListener.class);
- mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+ mockRunListener.testRunStarted(
+ EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+ EasyMock.eq(4),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
mockRunListener.testStarted((TestDescription) EasyMock.anyObject(), EasyMock.anyLong());
EasyMock.expectLastCall().times(4);
mockRunListener.testEnded(
(TestDescription) EasyMock.anyObject(),
EasyMock.anyLong(),
- (HashMap<String, Metric>) EasyMock.anyObject());
+ EasyMock.<HashMap<String, Metric>>anyObject());
EasyMock.expectLastCall().times(4);
mockRunListener.testRunEnded(
- EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+ EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
EasyMock.expectLastCall().times(1);
mockRunListener.testIgnored((TestDescription) EasyMock.anyObject());
EasyMock.expectLastCall();
@@ -128,18 +132,22 @@
String[] contents = readInFile(SUBPROC_OUTPUT_FILE_2);
ITestInvocationListener mockRunListener =
EasyMock.createMock(ITestInvocationListener.class);
- mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+ mockRunListener.testRunStarted(
+ EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+ EasyMock.eq(4),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
mockRunListener.testStarted((TestDescription) EasyMock.anyObject(), EasyMock.anyLong());
EasyMock.expectLastCall().times(4);
mockRunListener.testEnded(
(TestDescription) EasyMock.anyObject(),
EasyMock.anyLong(),
- (HashMap<String, Metric>) EasyMock.anyObject());
+ EasyMock.<HashMap<String, Metric>>anyObject());
EasyMock.expectLastCall().times(3);
mockRunListener.testRunFailed((String)EasyMock.anyObject());
EasyMock.expectLastCall().times(1);
mockRunListener.testRunEnded(
- EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+ EasyMock.anyLong(), EasyMock.<HashMap<String, Metric>>anyObject());
EasyMock.expectLastCall().times(1);
mockRunListener.testIgnored((TestDescription) EasyMock.anyObject());
EasyMock.expectLastCall();
@@ -165,11 +173,15 @@
public void testParse_testNotStarted() throws Exception {
ITestInvocationListener mockRunListener =
EasyMock.createMock(ITestInvocationListener.class);
- mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+ mockRunListener.testRunStarted(
+ EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+ EasyMock.eq(4),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
mockRunListener.testEnded(
(TestDescription) EasyMock.anyObject(),
EasyMock.anyLong(),
- (HashMap<String, Metric>) EasyMock.anyObject());
+ EasyMock.<HashMap<String, Metric>>anyObject());
EasyMock.expectLastCall().times(1);
EasyMock.replay(mockRunListener);
File tmp = FileUtil.createTempFile("sub", "unit");
@@ -200,11 +212,15 @@
public void testParse_noTimeStamp() throws Exception {
ITestInvocationListener mockRunListener =
EasyMock.createMock(ITestInvocationListener.class);
- mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+ mockRunListener.testRunStarted(
+ EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+ EasyMock.eq(4),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
mockRunListener.testStarted(EasyMock.anyObject());
mockRunListener.testEnded(
(TestDescription) EasyMock.anyObject(),
- (HashMap<String, Metric>) EasyMock.anyObject());
+ EasyMock.<HashMap<String, Metric>>anyObject());
EasyMock.expectLastCall().times(1);
EasyMock.replay(mockRunListener);
File tmp = FileUtil.createTempFile("sub", "unit");
@@ -272,11 +288,15 @@
public void testParser_receiveFromSocket() throws Exception {
ITestInvocationListener mockRunListener =
EasyMock.createMock(ITestInvocationListener.class);
- mockRunListener.testRunStarted("arm64-v8a CtsGestureTestCases", 4);
+ mockRunListener.testRunStarted(
+ EasyMock.eq("arm64-v8a CtsGestureTestCases"),
+ EasyMock.eq(4),
+ EasyMock.eq(0),
+ EasyMock.anyLong());
mockRunListener.testEnded(
(TestDescription) EasyMock.anyObject(),
EasyMock.anyLong(),
- (HashMap<String, Metric>) EasyMock.anyObject());
+ EasyMock.<HashMap<String, Metric>>anyObject());
EasyMock.expectLastCall().times(1);
EasyMock.replay(mockRunListener);
SubprocessTestResultsParser resultParser = null;
@@ -473,7 +493,8 @@
public void testParse_logAssociation() throws Exception {
ILogSaverListener mockRunListener = EasyMock.createMock(ILogSaverListener.class);
Capture<LogFile> capture = new Capture<>();
- mockRunListener.logAssociation(EasyMock.eq("dataname"), EasyMock.capture(capture));
+ mockRunListener.logAssociation(
+ EasyMock.eq("subprocess-dataname"), EasyMock.capture(capture));
EasyMock.replay(mockRunListener);
LogFile logFile = new LogFile("path", "url", LogDataType.TEXT);
File serializedLogFile = null;
diff --git a/tests/src/com/android/tradefed/util/TarUtilTest.java b/tests/src/com/android/tradefed/util/TarUtilTest.java
index cc9c267..4870b4d 100644
--- a/tests/src/com/android/tradefed/util/TarUtilTest.java
+++ b/tests/src/com/android/tradefed/util/TarUtilTest.java
@@ -88,6 +88,24 @@
}
/**
+ * Test that {TarUtil#extractTarGzipToTemp(File, String)} can extract properly a tar.gz file.
+ */
+ @Test
+ public void testExtractTarGzipToTemp() throws Exception {
+ InputStream logTarGz = getClass().getResourceAsStream(EMMA_METADATA_RESOURCE_PATH);
+ File tarGzFile = FileUtil.createTempFile("extract_tar_gz_test", ".tar.gz");
+ File tempDir = null;
+ try {
+ FileUtil.writeToFile(logTarGz, tarGzFile);
+ tempDir = TarUtil.extractTarGzipToTemp(tarGzFile, "extract_tar_gz_test");
+ Assert.assertEquals(2, tempDir.list().length);
+ } finally {
+ FileUtil.recursiveDelete(tempDir);
+ FileUtil.deleteFile(tarGzFile);
+ }
+ }
+
+ /**
* Test that {TarUtil#extractAndLog(ITestLogger, File, String)} can untar properly a tar file
* and export its content.
*/
diff --git a/tests/src/com/android/tradefed/util/UserUtilTest.java b/tests/src/com/android/tradefed/util/UserUtilTest.java
deleted file mode 100644
index ee18375..0000000
--- a/tests/src/com/android/tradefed/util/UserUtilTest.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.util;
-
-import com.android.tradefed.util.UserUtil.UserType;
-
-import java.util.Arrays;
-import java.util.ArrayList;
-
-import static org.junit.Assert.fail;
-
-import com.android.tradefed.device.ITestDevice;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-
-/** Unit tests for {@link UserChecker} */
-@RunWith(JUnit4.class)
-public class UserUtilTest {
- @Test
- public void testSwitchToUserSystemSuccess() throws Exception {
- int currentUser = 12;
-
- ITestDevice device = mock(ITestDevice.class);
- when(device.switchUser(UserUtil.USER_SYSTEM)).thenReturn(true);
-
- UserUtil.switchToUserType(device, UserType.SYSTEM);
- verify(device, times(1)).switchUser(UserUtil.USER_SYSTEM);
- }
-
- @Test
- public void testSwitchToUserSystemFail() throws Exception {
- int currentUser = 12;
-
- ITestDevice device = mock(ITestDevice.class);
- when(device.switchUser(UserUtil.USER_SYSTEM)).thenReturn(false);
-
- try {
- UserUtil.switchToUserType(device, UserType.SYSTEM);
- fail();
- } catch (UserUtil.UserSwitchFailedException _expected) {
- }
- verify(device, times(1)).switchUser(UserUtil.USER_SYSTEM);
- }
-
- @Test
- public void testSwitchToSecondaryUserCurrent() throws Exception {
- int currentUser = 10;
-
- ITestDevice device = mock(ITestDevice.class);
- when(device.getCurrentUser()).thenReturn(currentUser);
- when(device.isUserSecondary(currentUser)).thenReturn(true);
-
- UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
- verify(device, never()).switchUser(currentUser);
- }
-
- @Test
- public void testSwitchToSecondaryUserExists() throws Exception {
- ITestDevice device = mock(ITestDevice.class);
- when(device.getCurrentUser()).thenReturn(0);
- mockListUsers(device, new Integer[] {0, 10});
- when(device.isUserSecondary(10)).thenReturn(true);
- when(device.switchUser(10)).thenReturn(true);
-
- UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
- verify(device, times(1)).switchUser(10);
- }
-
- @Test
- /** Validate that invalid user types will be skipped as secondaries. */
- public void testSwitchToSecondaryUserWithInvalid() throws Exception {
- ITestDevice device = mock(ITestDevice.class);
- when(device.getCurrentUser()).thenReturn(0);
- mockListUsers(device, new Integer[] {0, 10, 11, 12});
- when(device.isUserSecondary(10)).thenReturn(false);
- when(device.isUserSecondary(11)).thenReturn(false);
- when(device.isUserSecondary(12)).thenReturn(true);
- when(device.switchUser(12)).thenReturn(true);
-
- UserUtil.switchToUserType(device, UserUtil.UserType.SECONDARY);
- verify(device, times(1)).switchUser(12);
- }
-
- @Test
- public void testSwitchToPrimaryUserNonSystem() throws Exception {
- ITestDevice device = mock(ITestDevice.class);
- when(device.getCurrentUser()).thenReturn(0);
- when(device.getPrimaryUserId()).thenReturn(10);
- when(device.switchUser(10)).thenReturn(true);
-
- UserUtil.switchToUserType(device, UserUtil.UserType.PRIMARY);
- verify(device, times(1)).switchUser(10);
- }
-
- // Helpers
-
- private void mockListUsers(ITestDevice device, Integer[] userIds) throws Exception {
- when(device.listUsers()).thenReturn(new ArrayList<Integer>(Arrays.asList(userIds)));
- }
-}
diff --git a/tests/src/com/android/tradefed/util/ZipUtilTest.java b/tests/src/com/android/tradefed/util/ZipUtilTest.java
index 4e4fb2f..51dc090 100644
--- a/tests/src/com/android/tradefed/util/ZipUtilTest.java
+++ b/tests/src/com/android/tradefed/util/ZipUtilTest.java
@@ -15,13 +15,24 @@
*/
package com.android.tradefed.util;
+import com.android.tradefed.util.zip.CentralDirectoryInfo;
+import com.android.tradefed.util.zip.EndCentralDirectoryInfo;
+import com.android.tradefed.util.zip.LocalFileHeader;
+
import junit.framework.TestCase;
+import java.io.BufferedReader;
import java.io.File;
+import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.zip.ZipFile;
@@ -191,6 +202,159 @@
}
}
+ public void testPartipUnzip() throws Exception {
+ File partialZipFile = null;
+ File tmpDir = null;
+ Set<PosixFilePermission> permissions;
+ try {
+ // The zip file is small, read the whole file and assume it's partial.
+ // This does not affect testing the behavior of partial unzipping.
+ partialZipFile = getTestDataFile("partial_zip");
+ EndCentralDirectoryInfo endCentralDirInfo = new EndCentralDirectoryInfo(partialZipFile);
+ List<CentralDirectoryInfo> zipEntries =
+ ZipUtil.getZipCentralDirectoryInfos(
+ partialZipFile,
+ endCentralDirInfo,
+ endCentralDirInfo.getCentralDirOffset());
+ // The zip file has 3 folders, 4 files.
+ assertEquals(7, zipEntries.size());
+
+ CentralDirectoryInfo zipEntry;
+ LocalFileHeader localFileHeader;
+ File targetFile;
+ tmpDir = FileUtil.createTempDir("partial_unzip");
+
+ // Unzip empty file
+ zipEntry =
+ zipEntries
+ .stream()
+ .filter(e -> e.getFileName().equals("empty_file"))
+ .findFirst()
+ .get();
+ targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+ localFileHeader =
+ new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+ ZipUtil.unzipPartialZipFile(
+ partialZipFile,
+ targetFile,
+ zipEntry,
+ localFileHeader,
+ zipEntry.getLocalHeaderOffset());
+ // Verify file permissions - readonly - 644 rw-r--r--
+ permissions = Files.getPosixFilePermissions(targetFile.toPath());
+ assertEquals(PosixFilePermissions.fromString("rw-r--r--"), permissions);
+
+ // Unzip text file
+ zipEntry =
+ zipEntries
+ .stream()
+ .filter(e -> e.getFileName().equals("large_text/file.txt"))
+ .findFirst()
+ .get();
+ targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+ localFileHeader =
+ new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+ ZipUtil.unzipPartialZipFile(
+ partialZipFile,
+ targetFile,
+ zipEntry,
+ localFileHeader,
+ zipEntry.getLocalHeaderOffset());
+ // Verify CRC
+ long crc = FileUtil.calculateCrc32(targetFile);
+ assertEquals(4146093769L, crc);
+ try (BufferedReader br = new BufferedReader(new FileReader(targetFile))) {
+ String line = br.readLine();
+ assertTrue(line.endsWith("this is a text file."));
+ } catch (IOException e) {
+ // fail if the file is corrupt in any way
+ fail("failed reading text file");
+ }
+ // Verify file permissions - read/write - 666 rw-rw-rw-
+ permissions = Files.getPosixFilePermissions(targetFile.toPath());
+ assertEquals(PosixFilePermissions.fromString("rw-rw-rw-"), permissions);
+
+ // Verify file permissions - executable - 755 rwxr-xr-x
+ zipEntry =
+ zipEntries
+ .stream()
+ .filter(e -> e.getFileName().equals("executable/executable_file"))
+ .findFirst()
+ .get();
+ targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+ localFileHeader =
+ new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+ ZipUtil.unzipPartialZipFile(
+ partialZipFile,
+ targetFile,
+ zipEntry,
+ localFileHeader,
+ zipEntry.getLocalHeaderOffset());
+ permissions = Files.getPosixFilePermissions(targetFile.toPath());
+ assertEquals(PosixFilePermissions.fromString("rwxr-xr-x"), permissions);
+
+ // Verify file permissions - readonly - 444 r--r--r--
+ zipEntry =
+ zipEntries
+ .stream()
+ .filter(e -> e.getFileName().equals("read_only/readonly_file"))
+ .findFirst()
+ .get();
+ targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+ localFileHeader =
+ new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+ ZipUtil.unzipPartialZipFile(
+ partialZipFile,
+ targetFile,
+ zipEntry,
+ localFileHeader,
+ zipEntry.getLocalHeaderOffset());
+ permissions = Files.getPosixFilePermissions(targetFile.toPath());
+ assertEquals(PosixFilePermissions.fromString("r--r--r--"), permissions);
+
+ // Verify folder permissions - readonly - 744 rwxr--r--
+ zipEntry =
+ zipEntries
+ .stream()
+ .filter(e -> e.getFileName().equals("read_only/"))
+ .findFirst()
+ .get();
+ targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+ localFileHeader =
+ new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+ ZipUtil.unzipPartialZipFile(
+ partialZipFile,
+ targetFile,
+ zipEntry,
+ localFileHeader,
+ zipEntry.getLocalHeaderOffset());
+ permissions = Files.getPosixFilePermissions(targetFile.toPath());
+ assertEquals(PosixFilePermissions.fromString("rwxr--r--"), permissions);
+
+ // Verify folder permissions - read/write - 755 rwxr-xr-x
+ zipEntry =
+ zipEntries
+ .stream()
+ .filter(e -> e.getFileName().equals("large_text/"))
+ .findFirst()
+ .get();
+ targetFile = new File(Paths.get(tmpDir.toString(), zipEntry.getFileName()).toString());
+ localFileHeader =
+ new LocalFileHeader(partialZipFile, (int) zipEntry.getLocalHeaderOffset());
+ ZipUtil.unzipPartialZipFile(
+ partialZipFile,
+ targetFile,
+ zipEntry,
+ localFileHeader,
+ zipEntry.getLocalHeaderOffset());
+ permissions = Files.getPosixFilePermissions(targetFile.toPath());
+ assertEquals(PosixFilePermissions.fromString("rwxr-xr-x"), permissions);
+ } finally {
+ FileUtil.deleteFile(partialZipFile);
+ FileUtil.recursiveDelete(tmpDir);
+ }
+ }
+
// Helpers
private File createTempDir(String prefix) throws IOException {
return createTempDir(prefix, null);
diff --git a/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java b/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
index 464780a..bd9b379 100644
--- a/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
+++ b/tests/src/com/android/tradefed/util/sl4a/Sl4aClientTest.java
@@ -36,6 +36,7 @@
*/
public class Sl4aClientTest {
+ private static final String DEVICE_SERIAL = "54321";
private Sl4aClient mClient = null;
private FakeSocketServerHelper mDeviceServer;
private ITestDevice mMockDevice;
@@ -125,6 +126,7 @@
*/
private void setupStartExpectation() throws DeviceNotAvailableException {
final String cmd = String.format(Sl4aClient.SL4A_LAUNCH_CMD, mDeviceServer.getPort());
+ EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn(DEVICE_SERIAL);
EasyMock.expect(mMockDevice.executeShellCommand(cmd))
.andReturn("");
EasyMock.expect(mMockDevice.executeShellCommand(Sl4aClient.IS_SL4A_RUNNING_CMD))
diff --git a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
index 56cfa8c..8c53c17 100644
--- a/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
+++ b/tests/src/com/android/tradefed/util/testmapping/TestMappingTest.java
@@ -32,6 +32,8 @@
import java.io.File;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
@@ -504,4 +506,35 @@
FileUtil.recursiveDelete(tempDir);
}
}
+
+ /** Test for {@link TestMapping#removeComments()} for removing comments in TEST_MAPPING file. */
+ @Test
+ public void testRemoveComments() throws Exception {
+ String jsonString = getJsonStringByName("test_mapping_with_comments1");
+ String goldenString = getJsonStringByName("test_mapping_golden1");
+ assertEquals(TestMapping.removeComments(jsonString), goldenString);
+ }
+
+ /** Test for {@link TestMapping#removeComments()} for removing comments in TEST_MAPPING file. */
+ @Test
+ public void testRemoveComments2() throws Exception {
+ String jsonString = getJsonStringByName("test_mapping_with_comments2");
+ String goldenString = getJsonStringByName("test_mapping_golden2");
+ assertEquals(TestMapping.removeComments(jsonString), goldenString);
+ }
+
+ private String getJsonStringByName(String fileName) throws Exception {
+ File tempDir = null;
+ try {
+ tempDir = FileUtil.createTempDir("test_mapping");
+ File srcDir = FileUtil.createTempDir("src", tempDir);
+ String srcFile = File.separator + TEST_DATA_DIR + File.separator + fileName;
+ InputStream resourceStream = this.getClass().getResourceAsStream(srcFile);
+ FileUtil.saveResourceFile(resourceStream, srcDir, TEST_MAPPING);
+ Path file = Paths.get(srcDir.getAbsolutePath(), TEST_MAPPING);
+ return String.join("\n", Files.readAllLines(file, StandardCharsets.UTF_8));
+ } finally {
+ FileUtil.recursiveDelete(tempDir);
+ }
+ }
}
diff --git a/tests/src/com/android/tradefed/util/xml/AndroidManifestWriterTest.java b/tests/src/com/android/tradefed/util/xml/AndroidManifestWriterTest.java
deleted file mode 100644
index 74488cb..0000000
--- a/tests/src/com/android/tradefed/util/xml/AndroidManifestWriterTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.util.xml;
-
-import com.android.tradefed.util.FileUtil;
-
-import junit.framework.TestCase;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Unit tests for {@link AndroidManifestWriter}.
- */
-public class AndroidManifestWriterTest extends TestCase {
-
- private static final String TEST_SDK_VERSION = "99";
-
- /**
- * Success case test for {@link AndroidManifestWriter} when minSdkVersion is already set.
- */
- public void testSetMinSdkVersion() throws Exception {
- assertMinSdkChange("AndroidManifest_usessdk.xml", "AndroidManifest_usessdk_result.xml");
- }
-
- /**
- * Success case test for {@link AndroidManifestWriter} when minSdkVersion is not present in xml.
- */
- public void testSetMinSdkVersion_missing() throws Exception {
- assertMinSdkChange("AndroidManifest_missing.xml", "AndroidManifest_missing_result.xml");
- }
-
- /**
- * Negative case test for {@link AndroidManifestWriter} when xml is invalid.
- */
- public void testSetMinSdkVersion_invalid() throws IOException {
- File manifest = extractTestXml("AndroidManifest_invalid.xml");
- try {
- assertNull(AndroidManifestWriter.parse(manifest.getAbsolutePath()));
- } finally {
- FileUtil.deleteFile(manifest);
- }
- }
-
- private void assertMinSdkChange(String inputFileName, String resultFileName)
- throws IOException {
- File manifest = extractTestXml(inputFileName);
- File expectedResultFile = extractTestXml(resultFileName);
- try {
- AndroidManifestWriter writer = AndroidManifestWriter.parse(manifest.getAbsolutePath());
- assertNotNull(writer);
- writer.setMinSdkVersion(TEST_SDK_VERSION);
- assertTrue(String.format("File contents of %s and %s are not equal", inputFileName,
- resultFileName), FileUtil.compareFileContents(manifest, expectedResultFile));
- } finally {
- FileUtil.deleteFile(manifest);
- FileUtil.deleteFile(expectedResultFile);
- }
- }
-
- /**
- * Helper method to extract a test data file from current jar, and store it as a file on local
- * disk.
- *
- * @param fileName the base file name
- * @return the {@link File}
- * @throws IOException
- */
- private File extractTestXml(String fileName) throws IOException {
- InputStream testStream = getClass().getResourceAsStream(File.separator + "xml" +
- File.separator + fileName);
- assertNotNull(testStream);
- File tmpFile = FileUtil.createTempFile(fileName, ".xml");
- FileUtil.writeToFile(testStream, tmpFile);
- return tmpFile;
- }
-}
diff --git a/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java b/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java
new file mode 100644
index 0000000..5cd860d
--- /dev/null
+++ b/tests/src/com/android/tradefed/util/zip/MergedZipEntryCollectionTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.util.zip;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Unit tests for {@link MergedZipEntryCollection} */
+@RunWith(JUnit4.class)
+public class MergedZipEntryCollectionTest {
+
+ private static final int COMPRESSED_SIZE = 2 * MergedZipEntryCollection.MAX_GAP;
+
+ /** Test that zip entries are merged into sections as expected due to small gap in size. */
+ @Test
+ public void testMergeZipEntries_smallGap() throws Exception {
+ List<CentralDirectoryInfo> entries = new ArrayList<>();
+ long startOffset = 10;
+ CentralDirectoryInfo info = createZipEntry(startOffset);
+ entries.add(info);
+ startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+ // Create a gap smaller than MAX_GAP.
+ startOffset += MergedZipEntryCollection.MAX_GAP / 2;
+ info = createZipEntry(startOffset);
+ entries.add(info);
+ startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+ List<MergedZipEntryCollection> collections =
+ MergedZipEntryCollection.CreateCollections(entries);
+ assertEquals(1, collections.size());
+ assertEquals(2, collections.get(0).getZipEntries().size());
+ assertEquals(10, collections.get(0).getStartOffset());
+ assertEquals(22598, collections.get(0).getEndOffset());
+ }
+
+ /** Test that zip entries are not merged due to large gap in size. */
+ @Test
+ public void testMergeZipEntries_largeGap() throws Exception {
+ List<CentralDirectoryInfo> entries = new ArrayList<>();
+ long startOffset = 10;
+ CentralDirectoryInfo info = createZipEntry(startOffset);
+ entries.add(info);
+ startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+ // Create a gap larger than MAX_GAP.
+ startOffset += MergedZipEntryCollection.MAX_GAP * 2;
+ info = createZipEntry(startOffset);
+ entries.add(info);
+ startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+ List<MergedZipEntryCollection> collections =
+ MergedZipEntryCollection.CreateCollections(entries);
+ assertEquals(2, collections.size());
+ assertEquals(1, collections.get(0).getZipEntries().size());
+ assertEquals(10, collections.get(0).getStartOffset());
+ assertEquals(10280, collections.get(0).getEndOffset());
+ assertEquals(18472, collections.get(1).getStartOffset());
+ assertEquals(28742, collections.get(1).getEndOffset());
+ }
+
+ /** Test that zip entries are merged into a single section due to a small gap in percentage. */
+ @Test
+ public void testMergeZipEntries_smallGapPercent() throws Exception {
+ List<CentralDirectoryInfo> entries = new ArrayList<>();
+ long startOffset = 10;
+ // Create enough entries so the gap size is greater than MAX_GAP.
+ for (int i = 0; i < 10; i++) {
+ CentralDirectoryInfo info = createZipEntry(startOffset);
+ entries.add(info);
+ startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+ }
+
+ // Create a gap smaller than MAX_GAP_PERCENTAGE
+ startOffset += (long) (startOffset * MergedZipEntryCollection.MAX_GAP_PERCENTAGE) - 1024;
+ CentralDirectoryInfo info = createZipEntry(startOffset);
+ entries.add(info);
+ startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+ List<MergedZipEntryCollection> collections =
+ MergedZipEntryCollection.CreateCollections(entries);
+ assertEquals(1, collections.size());
+ assertEquals(11, collections.get(0).getZipEntries().size());
+ assertEquals(10, collections.get(0).getStartOffset());
+ assertEquals(127362, collections.get(0).getEndOffset());
+ }
+
+ /** Test that zip entries are not merged due to a large gap in percentage. */
+ @Test
+ public void testMergeZipEntries_largeGapPercent() throws Exception {
+ List<CentralDirectoryInfo> entries = new ArrayList<>();
+ long startOffset = 10;
+ // Create enough entries so the gap size is greater than MAX_GAP.
+ for (int i = 0; i < 10; i++) {
+ CentralDirectoryInfo info = createZipEntry(startOffset);
+ entries.add(info);
+ startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+ }
+
+ // Create a gap larger than MAX_GAP_PERCENTAGE
+ startOffset += (long) (startOffset * MergedZipEntryCollection.MAX_GAP_PERCENTAGE * 3);
+ CentralDirectoryInfo info = createZipEntry(startOffset);
+ entries.add(info);
+ startOffset += info.getCompressedSize() + MergedZipEntryCollection.HEADER_SIZE;
+
+ List<MergedZipEntryCollection> collections =
+ MergedZipEntryCollection.CreateCollections(entries);
+ assertEquals(2, collections.size());
+ assertEquals(10, collections.get(0).getZipEntries().size());
+ assertEquals(10, collections.get(0).getStartOffset());
+ assertEquals(102710, collections.get(0).getEndOffset());
+ assertEquals(1, collections.get(1).getZipEntries().size());
+ assertEquals(148929, collections.get(1).getStartOffset());
+ assertEquals(159199, collections.get(1).getEndOffset());
+ }
+
+ /** Create a {@link CentralDirectoryInfo} object for testing. */
+ private CentralDirectoryInfo createZipEntry(long startOffset) {
+ CentralDirectoryInfo info = new CentralDirectoryInfo();
+ info.setLocalHeaderOffset(startOffset);
+ info.setCompressedSize(COMPRESSED_SIZE);
+ return info;
+ }
+}
diff --git a/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java b/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
index 5edb2d4..f59dc1f 100644
--- a/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
+++ b/util-apps/WifiUtil/src/com/android/tradefed/utils/wifi/WifiConnector.java
@@ -22,6 +22,7 @@
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
+import android.os.SystemClock;
import android.util.Log;
import org.apache.http.client.HttpClient;
@@ -88,12 +89,12 @@
throw new WifiException(
String.format("Failed %s due to invalid timeout (%d ms)", description, timeout));
}
- long startTime = System.currentTimeMillis();
+ long startTime = SystemClock.uptimeMillis();
long endTime = startTime + timeout;
try {
- while (System.currentTimeMillis() < endTime) {
+ while (SystemClock.uptimeMillis() < endTime) {
if (checker.call()) {
- long elapsed = System.currentTimeMillis() - startTime;
+ long elapsed = SystemClock.uptimeMillis() - startTime;
Log.i(TAG, String.format(
"Time elapsed waiting for %s: %d ms", description, elapsed));
return elapsed;
diff --git a/verify.sh b/verify.sh
deleted file mode 100755
index 7484194..0000000
--- a/verify.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# A helper script that launches Trade Federation's "Verify" entrypoint, to perform
-# standalone command file verification
-
-shdir=`dirname $0`/
-source "${shdir}/script_help.sh"
-# At this point, we're guaranteed to have the right Java version, and the following
-# env variables will be set, if appropriate:
-# JAVA_VERSION, RDBG_FLAG, TF_PATH, TRADEFED_OPTS
-
-
-# Note: must leave $RDBG_FLAG and $TRADEFED_OPTS unquoted so that they go away when unset
-java $RDBG_FLAG -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow $TRADEFED_OPTS \
- -cp "${TF_PATH}" com.android.tradefed.command.Verify "$@"