Atest: move src tree to tools/asuite

This is just a copy of tools/tradefederation/core/atest. Note that
tools/tradefederation/core/atest/Android.bp is not copied; also, the
asuite_lib_test/ is not copied to avoid duplicate target names.
we'll deal with modules at the last phase of migration.

Bug: b/144140286

Test: None
Change-Id: If8cdec8b7c35dd735195def470156687693a445b
diff --git a/atest/INTEGRATION_TESTS b/atest/INTEGRATION_TESTS
new file mode 100644
index 0000000..2bf986e
--- /dev/null
+++ b/atest/INTEGRATION_TESTS
@@ -0,0 +1,86 @@
+# TODO (b/121362882): Add deviceless tests when dry-run is ready.
+###[Test Finder: MODULE, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: MODULE and runner: AtestTradefedTestRunner###
+HelloWorldTests
+hello_world_test
+
+
+###[Test Finder: MODULE_FILE_PATH, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: MODULE_FILE_PATH and runner: AtestTradefedTestRunner###
+# frameworks/base/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java#testFlagChange
+# packages/apps/Bluetooth/tests/unit/Android.mk
+platform_testing/tests/example/native
+# platform_testing/tests/example/native/
+platform_testing/tests/example/native/Android.bp
+
+
+###[Test Finder: INTEGRATION_FILE_PATH, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: INTEGRATION_FILE_PATH and runner: AtestTradefedTestRunner###
+tools/tradefederation/core/res/config/native-benchmark.xml
+
+
+###[Test Finder: MODULE_CLASS, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: MODULE_CLASS and runner: AtestTradefedTestRunner###
+CtsAnimationTestCases:AnimatorTest
+CtsSampleDeviceTestCases:SampleDeviceTest#testSharedPreferences
+CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceReportLogTest
+
+
+###[Test Finder: QUALIFIED_CLASS, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: QUALIFIED_CLASS and runner: AtestTradefedTestRunner###
+# com.android.server.display.DisplayManagerServiceTest
+# com.android.server.wm.ScreenDecorWindowTests#testMultipleDecors
+
+
+###[Test Finder: MODULE_PACKAGE, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: MODULE_PACKAGE and runner: AtestTradefedTestRunner###
+CtsSampleDeviceTestCases:android.sample.cts
+
+
+###[Test Finder: PACKAGE, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: PACKAGE and runner: AtestTradefedTestRunner###
+android.animation.cts
+
+
+###[Test Finder: CLASS, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: CLASS and runner: AtestTradefedTestRunner###
+AnimatorTest
+
+
+###[Test Finder: CC_CLASS, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: CC_CLASS and runner: AtestTradefedTestRunner###
+PacketFragmenterTest
+# PacketFragmenterTest#test_no_fragment_necessary
+PacketFragmenterTest#test_no_fragment_necessary,test_ble_fragment_necessary
+
+
+###[Test Finder: INTEGRATION, Test Runner:AtestTradefedTestRunner]###
+###Purpose: Test with finder: INTEGRATION and runner: AtestTradefedTestRunner###
+native-benchmark
+
+
+###[Test Finder: MODULE, Test Runner: VtsTradefedTestRunner]####
+###Purpose: Test with finder: MODULE and runner: VtsTradefedTestRunner###
+VtsCodelabHelloWorldTest
+
+
+###[Test Finder: MODULE, Test Runner: RobolectricTestRunner]#####
+###Purpose: Test with finder: MODULE and runner: RobolectricTestRunner###
+CarMessengerRoboTests
+###Purpose: Test with input path for RobolectricTest###
+packages/apps/Car/Messenger/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java
+
+
+###[Test Finder: SUITE_PLAN, Test Runner: SuitePlanTestRunner]###
+###Purpose: Test with finder: SUITE_PLAN and runner: SuitePlanTestRunner###
+# cts-common
+
+
+###[Test Finder: SUITE_PLAN_FILE_PATH, Test Runner: SuitePlanTestRunner]###
+###Purpose: Test with finder: SUITE_PLAN_FILE_PATH and runner: SuitePlanTestRunner###
+# test/suite_harness/tools/cts-tradefed/res/config/cts.xml
+
+
+###[MULTIPLE-TESTS + AtestTradefedTestRunner]###
+###Purpose: Test with mixed testcases###
+CtsSampleDeviceTestCases CtsAnimationTestCases
diff --git a/atest/OWNERS b/atest/OWNERS
new file mode 100644
index 0000000..e41a595
--- /dev/null
+++ b/atest/OWNERS
@@ -0,0 +1,3 @@
[email protected]
[email protected]
[email protected]
diff --git a/atest/README.md b/atest/README.md
new file mode 100644
index 0000000..3824245
--- /dev/null
+++ b/atest/README.md
@@ -0,0 +1,6 @@
+# Atest
+
+The contents of this page have been moved to source.android.com.
+
+See:
+[Atest](https://source.android.com/compatibility/tests/development/atest)
diff --git a/atest/TEST_MAPPING b/atest/TEST_MAPPING
new file mode 100644
index 0000000..cebe409
--- /dev/null
+++ b/atest/TEST_MAPPING
@@ -0,0 +1,31 @@
+// 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/__init__.py b/atest/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/__init__.py
diff --git a/atest/asuite_metrics.py b/atest/asuite_metrics.py
new file mode 100644
index 0000000..8dcd7dc
--- /dev/null
+++ b/atest/asuite_metrics.py
@@ -0,0 +1,111 @@
+# Copyright 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.
+
+"""Asuite simple Metrics Functions"""
+
+import json
+import logging
+import os
+import uuid
+
+try:
+    # PYTHON2
+    from urllib2 import Request
+    from urllib2 import urlopen
+except ImportError:
+    # PYTHON3
+    from urllib.request import Request
+    from urllib.request import urlopen
+
+
+_JSON_HEADERS = {'Content-Type': 'application/json'}
+_METRICS_RESPONSE = 'done'
+_METRICS_TIMEOUT = 2 #seconds
+_META_FILE = os.path.join(os.path.expanduser('~'),
+                          '.config', 'asuite', '.metadata')
+_ANDROID_BUILD_TOP = 'ANDROID_BUILD_TOP'
+
+DUMMY_UUID = '00000000-0000-4000-8000-000000000000'
+
+
+#pylint: disable=broad-except
+def log_event(metrics_url, dummy_key_fallback=True, **kwargs):
+    """Base log event function for asuite backend.
+
+    Args:
+        metrics_url: String, URL to report metrics to.
+        dummy_key_fallback: Boolean, If True and unable to get grouping key,
+                            use a dummy key otherwise return out. Sometimes we
+                            don't want to return metrics for users we are
+                            unable to identify. Default True.
+        kwargs: Dict, additional fields we want to return metrics for.
+    """
+    try:
+        try:
+            key = str(_get_grouping_key())
+        except Exception:
+            if not dummy_key_fallback:
+                return
+            key = DUMMY_UUID
+        data = {'grouping_key': key,
+                'run_id': str(uuid.uuid4())}
+        if kwargs:
+            data.update(kwargs)
+        data = json.dumps(data)
+        request = Request(metrics_url, data=data,
+                          headers=_JSON_HEADERS)
+        response = urlopen(request, timeout=_METRICS_TIMEOUT)
+        content = response.read()
+        if content != _METRICS_RESPONSE:
+            raise Exception('Unexpected metrics response: %s' % content)
+    except Exception as e:
+        logging.debug('Exception sending metrics: %s', e)
+
+
+def _get_grouping_key():
+    """Get grouping key. Returns UUID.uuid4."""
+    if os.path.isfile(_META_FILE):
+        with open(_META_FILE) as f:
+            try:
+                return uuid.UUID(f.read(), version=4)
+            except ValueError:
+                logging.debug('malformed group_key in file, rewriting')
+    # TODO: Delete get_old_key() on 11/17/2018
+    key = _get_old_key() or uuid.uuid4()
+    dir_path = os.path.dirname(_META_FILE)
+    if os.path.isfile(dir_path):
+        os.remove(dir_path)
+    try:
+        os.makedirs(dir_path)
+    except OSError as e:
+        if not os.path.isdir(dir_path):
+            raise e
+    with open(_META_FILE, 'w+') as f:
+        f.write(str(key))
+    return key
+
+
+def _get_old_key():
+    """Get key from old meta data file if exists, else return None."""
+    old_file = os.path.join(os.environ[_ANDROID_BUILD_TOP],
+                            'tools/tradefederation/core/atest', '.metadata')
+    key = None
+    if os.path.isfile(old_file):
+        with open(old_file) as f:
+            try:
+                key = uuid.UUID(f.read(), version=4)
+            except ValueError:
+                logging.debug('error reading old key')
+        os.remove(old_file)
+    return key
diff --git a/atest/atest.py b/atest/atest.py
new file mode 100755
index 0000000..880288a
--- /dev/null
+++ b/atest/atest.py
@@ -0,0 +1,693 @@
+#!/usr/bin/env python
+#
+# Copyright 2017, 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.
+
+"""
+Command line utility for running Android tests through TradeFederation.
+
+atest helps automate the flow of building test modules across the Android
+code base and executing the tests via the TradeFederation test harness.
+
+atest is designed to support any test types that can be ran by TradeFederation.
+"""
+
+from __future__ import print_function
+
+import logging
+import os
+import sys
+import tempfile
+import time
+import platform
+
+from multiprocessing import Process
+
+import atest_arg_parser
+import atest_error
+import atest_execution_info
+import atest_utils
+import bug_detector
+import cli_translator
+# pylint: disable=import-error
+import constants
+import module_info
+import result_reporter
+import test_runner_handler
+
+from metrics import metrics
+from metrics import metrics_base
+from metrics import metrics_utils
+from test_runners import regression_test_runner
+from tools import atest_tools
+
+EXPECTED_VARS = frozenset([
+    constants.ANDROID_BUILD_TOP,
+    'ANDROID_TARGET_OUT_TESTCASES',
+    constants.ANDROID_OUT])
+TEST_RUN_DIR_PREFIX = "%Y%m%d_%H%M"
+CUSTOM_ARG_FLAG = '--'
+OPTION_NOT_FOR_TEST_MAPPING = (
+    'Option `%s` does not work for running tests in TEST_MAPPING files')
+
+DEVICE_TESTS = 'tests that require device'
+HOST_TESTS = 'tests that do NOT require device'
+RESULT_HEADER_FMT = '\nResults from %(test_type)s:'
+RUN_HEADER_FMT = '\nRunning %(test_count)d %(test_type)s.'
+TEST_COUNT = 'test_count'
+TEST_TYPE = 'test_type'
+# Tasks that must run in the build time but unable to build by soong.
+# (e.g subprocesses that invoke host commands.)
+EXTRA_TASKS = {
+    'index-targets': atest_tools.index_targets
+}
+
+
+def _run_extra_tasks(join=False):
+    """Execute EXTRA_TASKS with multiprocessing.
+
+    Args:
+        join: A boolean that indicates the process should terminate when
+        the main process ends or keep itself alive. True indicates the
+        main process will wait for all subprocesses finish while False represents
+        killing all subprocesses when the main process exits.
+    """
+    _running_procs = []
+    for task in EXTRA_TASKS.values():
+        proc = Process(target=task)
+        proc.daemon = not join
+        proc.start()
+        _running_procs.append(proc)
+    if join:
+        for proc in _running_procs:
+            proc.join()
+
+
+def _parse_args(argv):
+    """Parse command line arguments.
+
+    Args:
+        argv: A list of arguments.
+
+    Returns:
+        An argspace.Namespace class instance holding parsed args.
+    """
+    # Store everything after '--' in custom_args.
+    pruned_argv = argv
+    custom_args_index = None
+    if CUSTOM_ARG_FLAG in argv:
+        custom_args_index = argv.index(CUSTOM_ARG_FLAG)
+        pruned_argv = argv[:custom_args_index]
+    parser = atest_arg_parser.AtestArgParser()
+    parser.add_atest_args()
+    args = parser.parse_args(pruned_argv)
+    args.custom_args = []
+    if custom_args_index is not None:
+        args.custom_args = argv[custom_args_index+1:]
+    return args
+
+
+def _configure_logging(verbose):
+    """Configure the logger.
+
+    Args:
+        verbose: A boolean. If true display DEBUG level logs.
+    """
+    log_format = '%(asctime)s %(filename)s:%(lineno)s:%(levelname)s: %(message)s'
+    datefmt = '%Y-%m-%d %H:%M:%S'
+    if verbose:
+        logging.basicConfig(level=logging.DEBUG, format=log_format, datefmt=datefmt)
+    else:
+        logging.basicConfig(level=logging.INFO, format=log_format, datefmt=datefmt)
+
+
+def _missing_environment_variables():
+    """Verify the local environment has been set up to run atest.
+
+    Returns:
+        List of strings of any missing environment variables.
+    """
+    missing = filter(None, [x for x in EXPECTED_VARS if not os.environ.get(x)])
+    if missing:
+        logging.error('Local environment doesn\'t appear to have been '
+                      'initialized. Did you remember to run lunch? Expected '
+                      'Environment Variables: %s.', missing)
+    return missing
+
+
+def make_test_run_dir():
+    """Make the test run dir in ATEST_RESULT_ROOT.
+
+    Returns:
+        A string of the dir path.
+    """
+    if not os.path.exists(constants.ATEST_RESULT_ROOT):
+        os.makedirs(constants.ATEST_RESULT_ROOT)
+    ctime = time.strftime(TEST_RUN_DIR_PREFIX, time.localtime())
+    return tempfile.mkdtemp(prefix='%s_' % ctime, dir=constants.ATEST_RESULT_ROOT)
+
+
+def get_extra_args(args):
+    """Get extra args for test runners.
+
+    Args:
+        args: arg parsed object.
+
+    Returns:
+        Dict of extra args for test runners to utilize.
+    """
+    extra_args = {}
+    if args.wait_for_debugger:
+        extra_args[constants.WAIT_FOR_DEBUGGER] = None
+    steps = args.steps or constants.ALL_STEPS
+    if constants.INSTALL_STEP not in steps:
+        extra_args[constants.DISABLE_INSTALL] = None
+    # The key and its value of the dict can be called via:
+    # if args.aaaa:
+    #     extra_args[constants.AAAA] = args.aaaa
+    arg_maps = {'all_abi': constants.ALL_ABI,
+                'custom_args': constants.CUSTOM_ARGS,
+                'disable_teardown': constants.DISABLE_TEARDOWN,
+                'dry_run': constants.DRY_RUN,
+                'generate_baseline': constants.PRE_PATCH_ITERATIONS,
+                'generate_new_metrics': constants.POST_PATCH_ITERATIONS,
+                'host': constants.HOST,
+                'instant': constants.INSTANT,
+                'iterations': constants.ITERATIONS,
+                'rerun_until_failure': constants.RERUN_UNTIL_FAILURE,
+                'retry_any_failure': constants.RETRY_ANY_FAILURE,
+                'serial': constants.SERIAL,
+                'user_type': constants.USER_TYPE}
+    not_match = [k for k in arg_maps if k not in vars(args)]
+    if not_match:
+        raise AttributeError('%s object has no attribute %s'
+                             %(type(args).__name__, not_match))
+    extra_args.update({arg_maps.get(k): v for k, v in vars(args).items()
+                       if arg_maps.get(k) and v})
+    return extra_args
+
+
+def _get_regression_detection_args(args, results_dir):
+    """Get args for regression detection test runners.
+
+    Args:
+        args: parsed args object.
+        results_dir: string directory to store atest results.
+
+    Returns:
+        Dict of args for regression detection test runner to utilize.
+    """
+    regression_args = {}
+    pre_patch_folder = (os.path.join(results_dir, 'baseline-metrics') if args.generate_baseline
+                        else args.detect_regression.pop(0))
+    post_patch_folder = (os.path.join(results_dir, 'new-metrics') if args.generate_new_metrics
+                         else args.detect_regression.pop(0))
+    regression_args[constants.PRE_PATCH_FOLDER] = pre_patch_folder
+    regression_args[constants.POST_PATCH_FOLDER] = post_patch_folder
+    return regression_args
+
+
+def _validate_exec_mode(args, test_infos, host_tests=None):
+    """Validate all test execution modes are not in conflict.
+
+    Exit the program with error code if have device-only and host-only.
+    If no conflict and host side, add args.host=True.
+
+    Args:
+        args: parsed args object.
+        test_info: TestInfo object.
+        host_tests: True if all tests should be deviceless, False if all tests
+            should be device tests. Default is set to None, which means
+            tests can be either deviceless or device tests.
+    """
+    all_device_modes = [x.get_supported_exec_mode() for x in test_infos]
+    err_msg = None
+    # In the case of '$atest <device-only> --host', exit.
+    if (host_tests or args.host) and constants.DEVICE_TEST in all_device_modes:
+        err_msg = ('Test side and option(--host) conflict. Please remove '
+                   '--host if the test run on device side.')
+    # In the case of '$atest <host-only> <device-only> --host' or
+    # '$atest <host-only> <device-only>', exit.
+    if (constants.DEVICELESS_TEST in all_device_modes and
+            constants.DEVICE_TEST in all_device_modes):
+        err_msg = 'There are host-only and device-only tests in command.'
+    if host_tests is False and constants.DEVICELESS_TEST in all_device_modes:
+        err_msg = 'There are host-only tests in command.'
+    if err_msg:
+        logging.error(err_msg)
+        metrics_utils.send_exit_event(constants.EXIT_CODE_ERROR, logs=err_msg)
+        sys.exit(constants.EXIT_CODE_ERROR)
+    # In the case of '$atest <host-only>', we add --host to run on host-side.
+    # The option should only be overridden if `host_tests` is not set.
+    if not args.host and host_tests is None:
+        args.host = bool(constants.DEVICELESS_TEST in all_device_modes)
+
+
+def _validate_tm_tests_exec_mode(args, test_infos):
+    """Validate all test execution modes are not in conflict.
+
+    Split the tests in Test Mapping files into two groups, device tests and
+    deviceless tests running on host. Validate the tests' host setting.
+    For device tests, exit the program if any test is found for host-only.
+    For deviceless tests, exit the program if any test is found for device-only.
+
+    Args:
+        args: parsed args object.
+        test_info: TestInfo object.
+    """
+    device_test_infos, host_test_infos = _split_test_mapping_tests(
+        test_infos)
+    # No need to verify device tests if atest command is set to only run host
+    # tests.
+    if device_test_infos and not args.host:
+        _validate_exec_mode(args, device_test_infos, host_tests=False)
+    if host_test_infos:
+        _validate_exec_mode(args, host_test_infos, host_tests=True)
+
+
+def _will_run_tests(args):
+    """Determine if there are tests to run.
+
+    Currently only used by detect_regression to skip the test if just running regression detection.
+
+    Args:
+        args: parsed args object.
+
+    Returns:
+        True if there are tests to run, false otherwise.
+    """
+    return not (args.detect_regression and len(args.detect_regression) == 2)
+
+
+def _has_valid_regression_detection_args(args):
+    """Validate regression detection args.
+
+    Args:
+        args: parsed args object.
+
+    Returns:
+        True if args are valid
+    """
+    if args.generate_baseline and args.generate_new_metrics:
+        logging.error('Cannot collect both baseline and new metrics at the same time.')
+        return False
+    if args.detect_regression is not None:
+        if not args.detect_regression:
+            logging.error('Need to specify at least 1 arg for regression detection.')
+            return False
+        elif len(args.detect_regression) == 1:
+            if args.generate_baseline or args.generate_new_metrics:
+                return True
+            logging.error('Need to specify --generate-baseline or --generate-new-metrics.')
+            return False
+        elif len(args.detect_regression) == 2:
+            if args.generate_baseline:
+                logging.error('Specified 2 metric paths and --generate-baseline, '
+                              'either drop --generate-baseline or drop a path')
+                return False
+            if args.generate_new_metrics:
+                logging.error('Specified 2 metric paths and --generate-new-metrics, '
+                              'either drop --generate-new-metrics or drop a path')
+                return False
+            return True
+        else:
+            logging.error('Specified more than 2 metric paths.')
+            return False
+    return True
+
+
+def _has_valid_test_mapping_args(args):
+    """Validate test mapping args.
+
+    Not all args work when running tests in TEST_MAPPING files. Validate the
+    args before running the tests.
+
+    Args:
+        args: parsed args object.
+
+    Returns:
+        True if args are valid
+    """
+    is_test_mapping = atest_utils.is_test_mapping(args)
+    if not is_test_mapping:
+        return True
+    options_to_validate = [
+        (args.generate_baseline, '--generate-baseline'),
+        (args.detect_regression, '--detect-regression'),
+        (args.generate_new_metrics, '--generate-new-metrics'),
+    ]
+    for arg_value, arg in options_to_validate:
+        if arg_value:
+            logging.error(OPTION_NOT_FOR_TEST_MAPPING, arg)
+            return False
+    return True
+
+
+def _validate_args(args):
+    """Validate setups and args.
+
+    Exit the program with error code if any setup or arg is invalid.
+
+    Args:
+        args: parsed args object.
+    """
+    if _missing_environment_variables():
+        sys.exit(constants.EXIT_CODE_ENV_NOT_SETUP)
+    if args.generate_baseline and args.generate_new_metrics:
+        logging.error(
+            'Cannot collect both baseline and new metrics at the same time.')
+        sys.exit(constants.EXIT_CODE_ERROR)
+    if not _has_valid_regression_detection_args(args):
+        sys.exit(constants.EXIT_CODE_ERROR)
+    if not _has_valid_test_mapping_args(args):
+        sys.exit(constants.EXIT_CODE_ERROR)
+
+
+def _print_module_info_from_module_name(mod_info, module_name):
+    """print out the related module_info for a module_name.
+
+    Args:
+        mod_info: ModuleInfo object.
+        module_name: A string of module.
+
+    Returns:
+        True if the module_info is found.
+    """
+    title_mapping = {
+        constants.MODULE_PATH: "Source code path",
+        constants.MODULE_INSTALLED: "Installed path",
+        constants.MODULE_COMPATIBILITY_SUITES: "Compatibility suite"}
+    target_module_info = mod_info.get_module_info(module_name)
+    is_module_found = False
+    if target_module_info:
+        atest_utils.colorful_print(module_name, constants.GREEN)
+        for title_key in title_mapping.iterkeys():
+            atest_utils.colorful_print("\t%s" % title_mapping[title_key],
+                                       constants.CYAN)
+            for info_value in target_module_info[title_key]:
+                print("\t\t{}".format(info_value))
+        is_module_found = True
+    return is_module_found
+
+
+def _print_test_info(mod_info, test_infos):
+    """Print the module information from TestInfos.
+
+    Args:
+        mod_info: ModuleInfo object.
+        test_infos: A list of TestInfos.
+
+    Returns:
+        Always return EXIT_CODE_SUCCESS
+    """
+    for test_info in test_infos:
+        _print_module_info_from_module_name(mod_info, test_info.test_name)
+        atest_utils.colorful_print("\tRelated build targets", constants.MAGENTA)
+        print("\t\t{}".format(", ".join(test_info.build_targets)))
+        for build_target in test_info.build_targets:
+            if build_target != test_info.test_name:
+                _print_module_info_from_module_name(mod_info, build_target)
+        atest_utils.colorful_print("", constants.WHITE)
+    return constants.EXIT_CODE_SUCCESS
+
+
+def is_from_test_mapping(test_infos):
+    """Check that the test_infos came from TEST_MAPPING files.
+
+    Args:
+        test_infos: A set of TestInfos.
+
+    Returns:
+        True if the test infos are from TEST_MAPPING files.
+    """
+    return list(test_infos)[0].from_test_mapping
+
+
+def _split_test_mapping_tests(test_infos):
+    """Split Test Mapping tests into 2 groups: device tests and host tests.
+
+    Args:
+        test_infos: A set of TestInfos.
+
+    Returns:
+        A tuple of (device_test_infos, host_test_infos), where
+        device_test_infos: A set of TestInfos for tests that require device.
+        host_test_infos: A set of TestInfos for tests that do NOT require
+            device.
+    """
+    assert is_from_test_mapping(test_infos)
+    host_test_infos = set([info for info in test_infos if info.host])
+    device_test_infos = set([info for info in test_infos if not info.host])
+    return device_test_infos, host_test_infos
+
+
+# pylint: disable=too-many-locals
+def _run_test_mapping_tests(results_dir, test_infos, extra_args):
+    """Run all tests in TEST_MAPPING files.
+
+    Args:
+        results_dir: String directory to store atest results.
+        test_infos: A set of TestInfos.
+        extra_args: Dict of extra args to add to test run.
+
+    Returns:
+        Exit code.
+    """
+    device_test_infos, host_test_infos = _split_test_mapping_tests(test_infos)
+    # `host` option needs to be set to True to run host side tests.
+    host_extra_args = extra_args.copy()
+    host_extra_args[constants.HOST] = True
+    test_runs = [(host_test_infos, host_extra_args, HOST_TESTS)]
+    if extra_args.get(constants.HOST):
+        atest_utils.colorful_print(
+            'Option `--host` specified. Skip running device tests.',
+            constants.MAGENTA)
+    else:
+        test_runs.append((device_test_infos, extra_args, DEVICE_TESTS))
+
+    test_results = []
+    for tests, args, test_type in test_runs:
+        if not tests:
+            continue
+        header = RUN_HEADER_FMT % {TEST_COUNT: len(tests), TEST_TYPE: test_type}
+        atest_utils.colorful_print(header, constants.MAGENTA)
+        logging.debug('\n'.join([str(info) for info in tests]))
+        tests_exit_code, reporter = test_runner_handler.run_all_tests(
+            results_dir, tests, args, delay_print_summary=True)
+        atest_execution_info.AtestExecutionInfo.result_reporters.append(reporter)
+        test_results.append((tests_exit_code, reporter, test_type))
+
+    all_tests_exit_code = constants.EXIT_CODE_SUCCESS
+    failed_tests = []
+    for tests_exit_code, reporter, test_type in test_results:
+        atest_utils.colorful_print(
+            RESULT_HEADER_FMT % {TEST_TYPE: test_type}, constants.MAGENTA)
+        result = tests_exit_code | reporter.print_summary()
+        if result:
+            failed_tests.append(test_type)
+        all_tests_exit_code |= result
+
+    # List failed tests at the end as a reminder.
+    if failed_tests:
+        atest_utils.colorful_print(
+            '\n==============================', constants.YELLOW)
+        atest_utils.colorful_print(
+            '\nFollowing tests failed:', constants.MAGENTA)
+        for failure in failed_tests:
+            atest_utils.colorful_print(failure, constants.RED)
+
+    return all_tests_exit_code
+
+
+def _dry_run(results_dir, extra_args, test_infos):
+    """Only print the commands of the target tests rather than running them in actual.
+
+    Args:
+        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.
+
+    Args:
+        mod_info: ModuleInfo object.
+        suite: A string of suite name.
+    """
+    testable_modules = mod_info.get_testable_modules(suite)
+    print('\n%s' % atest_utils.colorize('%s Testable %s modules' % (
+        len(testable_modules), suite), constants.CYAN))
+    print('-------')
+    for module in sorted(testable_modules):
+        print('\t%s' % module)
+
+# pylint: disable=too-many-statements
+# pylint: disable=too-many-branches
+def main(argv, results_dir):
+    """Entry point of atest script.
+
+    Args:
+        argv: A list of arguments.
+        results_dir: A directory which stores the ATest execution information.
+
+    Returns:
+        Exit code.
+    """
+    args = _parse_args(argv)
+    _configure_logging(args.verbose)
+    _validate_args(args)
+    metrics_utils.get_start_time()
+    metrics.AtestStartEvent(
+        command_line=' '.join(argv),
+        test_references=args.tests,
+        cwd=os.getcwd(),
+        os=platform.platform())
+    if args.version:
+        if os.path.isfile(constants.VERSION_FILE):
+            with open(constants.VERSION_FILE) as version_file:
+                print(version_file.read())
+        return constants.EXIT_CODE_SUCCESS
+    mod_info = module_info.ModuleInfo(force_build=args.rebuild_module_info)
+    if args.rebuild_module_info:
+        _run_extra_tasks(join=True)
+    translator = cli_translator.CLITranslator(module_info=mod_info)
+    if args.list_modules:
+        _print_testable_modules(mod_info, args.list_modules)
+        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:
+            return constants.EXIT_CODE_TEST_NOT_FOUND
+        if not is_from_test_mapping(test_infos):
+            _validate_exec_mode(args, test_infos)
+        else:
+            _validate_tm_tests_exec_mode(args, test_infos)
+    if args.info:
+        return _print_test_info(mod_info, test_infos)
+    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:
+        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('')
+                          .get_test_runner_build_reqs())
+    # args.steps will be None if none of -bit set, else list of params set.
+    steps = args.steps if args.steps else constants.ALL_STEPS
+    if build_targets and constants.BUILD_STEP in steps:
+        if constants.TEST_STEP in steps and not args.rebuild_module_info:
+            # Run extra tasks along with build step concurrently. Note that
+            # Atest won't index targets when only "-b" is given(without -t).
+            _run_extra_tasks(join=False)
+        # Add module-info.json target to the list of build targets to keep the
+        # file up to date.
+        build_targets.add(mod_info.module_info_target)
+        # Build the deps-license to generate dependencies data in
+        # module-info.json.
+        build_targets.add(constants.DEPS_LICENSE)
+        build_env = dict(constants.ATEST_BUILD_ENV)
+        # The environment variables PROJ_PATH and DEP_PATH are necessary for the
+        # deps-license.
+        build_env.update(constants.DEPS_LICENSE_ENV)
+        build_start = time.time()
+        success = atest_utils.build(build_targets, verbose=args.verbose,
+                                    env_vars=build_env)
+        metrics.BuildFinishEvent(
+            duration=metrics_utils.convert_duration(time.time() - build_start),
+            success=success,
+            targets=build_targets)
+        if not success:
+            return constants.EXIT_CODE_BUILD_FAILURE
+    elif constants.TEST_STEP not in steps:
+        logging.warn('Install step without test step currently not '
+                     'supported, installing AND testing instead.')
+        steps.append(constants.TEST_STEP)
+    tests_exit_code = constants.EXIT_CODE_SUCCESS
+    test_start = time.time()
+    if constants.TEST_STEP in steps:
+        if not is_from_test_mapping(test_infos):
+            tests_exit_code, reporter = test_runner_handler.run_all_tests(
+                results_dir, test_infos, extra_args)
+            atest_execution_info.AtestExecutionInfo.result_reporters.append(reporter)
+        else:
+            tests_exit_code = _run_test_mapping_tests(
+                results_dir, test_infos, extra_args)
+    if args.detect_regression:
+        regression_args = _get_regression_detection_args(args, results_dir)
+        # TODO(b/110485713): Should not call run_tests here.
+        reporter = result_reporter.ResultReporter()
+        atest_execution_info.AtestExecutionInfo.result_reporters.append(reporter)
+        tests_exit_code |= regression_test_runner.RegressionTestRunner(
+            '').run_tests(
+                None, regression_args, reporter)
+    metrics.RunTestsFinishEvent(
+        duration=metrics_utils.convert_duration(time.time() - test_start))
+    preparation_time = atest_execution_info.preparation_time(test_start)
+    if preparation_time:
+        # Send the preparation time only if it's set.
+        metrics.RunnerFinishEvent(
+            duration=metrics_utils.convert_duration(preparation_time),
+            success=True,
+            runner_name=constants.TF_PREPARATION,
+            test=[])
+    if tests_exit_code != constants.EXIT_CODE_SUCCESS:
+        tests_exit_code = constants.EXIT_CODE_TEST_FAILURE
+    return tests_exit_code
+
+if __name__ == '__main__':
+    RESULTS_DIR = make_test_run_dir()
+    with atest_execution_info.AtestExecutionInfo(sys.argv[1:],
+                                                 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)
+        if result_file:
+            print('Execution detail has saved in %s' % result_file.name)
+    sys.exit(EXIT_CODE)
diff --git a/atest/atest_arg_parser.py b/atest/atest_arg_parser.py
new file mode 100644
index 0000000..a3a2440
--- /dev/null
+++ b/atest/atest_arg_parser.py
@@ -0,0 +1,423 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""
+Atest Argument Parser class for atest.
+"""
+
+import argparse
+
+import atest_utils
+import constants
+
+
+def _positive_int(value):
+    """Verify value by whether or not a positive integer.
+
+    Args:
+        value: A string of a command-line argument.
+
+    Returns:
+        int of value, if it is an positive integer.
+        Otherwise, raise argparse.ArgumentTypeError.
+    """
+    err_msg = "invalid positive int value: '%s'" % value
+    try:
+        converted_value = int(value)
+        if converted_value < 1:
+            raise argparse.ArgumentTypeError(err_msg)
+        return converted_value
+    except ValueError:
+        raise argparse.ArgumentTypeError(err_msg)
+
+
+class AtestArgParser(argparse.ArgumentParser):
+    """Atest wrapper of ArgumentParser."""
+
+    def __init__(self):
+        """Initialise an ArgumentParser instance."""
+        atest_utils.print_data_collection_notice()
+        super(AtestArgParser, self).__init__(
+            description=constants.HELP_DESC,
+            epilog=self.EPILOG_TEXT,
+            formatter_class=argparse.RawTextHelpFormatter)
+
+    def add_atest_args(self):
+        """A function that does ArgumentParser.add_argument()"""
+        self.add_argument('tests', nargs='*', help='Tests to build and/or run.')
+        self.add_argument('-b', '--build', action='append_const', dest='steps',
+                          const=constants.BUILD_STEP, help='Run a build.')
+        self.add_argument('-i', '--install', action='append_const', dest='steps',
+                          const=constants.INSTALL_STEP, help='Install an APK.')
+        self.add_argument('--info', action='store_true',
+                          help='Show module information.')
+        self.add_argument('--dry-run', action='store_true',
+                          help='Dry run atest without building, installing and running '
+                               'tests in real.')
+        self.add_argument('-t', '--test', action='append_const', dest='steps',
+                          const=constants.TEST_STEP,
+                          help='Run the tests. WARNING: Many test configs force cleanup '
+                               'of device after test run. In this case, -d must be used in '
+                               'previous test run to disable cleanup, for -t to work. '
+                               'Otherwise, device will need to be setup again with -i.')
+        self.add_argument('-s', '--serial', help='The device to run the test on.')
+        self.add_argument('-L', '--list-modules', help='List testable modules for the given suite.')
+        self.add_argument('-d', '--disable-teardown', action='store_true',
+                          help='Disables test teardown and cleanup.')
+        self.add_argument('-m', constants.REBUILD_MODULE_INFO_FLAG, action='store_true',
+                          help='Forces a rebuild of the module-info.json file. '
+                               'This may be necessary following a repo sync or '
+                               'when writing a new test.')
+        self.add_argument('-w', '--wait-for-debugger', action='store_true',
+                          help='Only for instrumentation tests. Waits for '
+                               'debugger prior to execution.')
+        self.add_argument('-v', '--verbose', action='store_true',
+                          help='Display DEBUG level logging.')
+        self.add_argument('-V', '--version', action='store_true',
+                          help='Display version string.')
+        self.add_argument('-a', '--all-abi', action='store_true',
+                          help='Set to run tests for all abi.')
+        self.add_argument('--generate-baseline', nargs='?', type=int, const=5, default=0,
+                          help='Generate baseline metrics, run 5 iterations by default. '
+                               'Provide an int argument to specify # iterations.')
+        self.add_argument('--generate-new-metrics', nargs='?', type=int, const=5, default=0,
+                          help='Generate new metrics, run 5 iterations by default. '
+                               'Provide an int argument to specify # iterations.')
+        self.add_argument('--detect-regression', nargs='*',
+                          help='Run regression detection algorithm. Supply '
+                               'path to baseline and/or new metrics folders.')
+        # Options related to module parameterization
+        self.add_argument('--instant', action='store_true',
+                          help='Run the instant_app version of the module, '
+                               'if the module supports it. Note: running a test '
+                               'that does not support instant with --instant '
+                               'will result in nothing running.')
+        self.add_argument('--user-type', help='Run test with specific user type.'
+                                              'E.g. --user-type secondary_user')
+        # Options related to Test Mapping
+        self.add_argument('-p', '--test-mapping', action='store_true',
+                          help='Run tests in TEST_MAPPING files.')
+        self.add_argument('--include-subdirs', action='store_true',
+                          help='Include tests in TEST_MAPPING files in sub directories.')
+        # Options related to deviceless testing.
+        self.add_argument('--host', action='store_true',
+                          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.')
+        # A group of options for rerun strategy. They are mutually exclusive in a command line.
+        group = self.add_mutually_exclusive_group()
+        # Option for rerun tests for the specified number iterations.
+        group.add_argument('--iterations', nargs='?',
+                           type=_positive_int, const=10, default=0,
+                           metavar='MAX_ITERATIONS',
+                           help='Rerun all tests, run 10 iterations by default. '
+                                'Accept a positive int for # iterations.')
+        group.add_argument('--rerun-until-failure', nargs='?',
+                           type=_positive_int, const=10, default=0,
+                           metavar='MAX_ITERATIONS',
+                           help='Rerun all tests until a failure occurs or max iterations is '
+                                'reached, run 10 iterations by default. '
+                                'Accept a positive int for # iterations.')
+        group.add_argument('--retry-any-failure', nargs='?',
+                           type=_positive_int, const=10, default=0,
+                           metavar='MAX_ITERATIONS',
+                           help='Rerun failed tests until passed or max iterations is reached, '
+                                'run 10 iterations by default. '
+                                'Accept a positive int for # iterations.')
+        # 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='*',
+                          help='Specify custom args for the test runners. '
+                               'Everything after -- will be consumed as custom args.')
+
+    def get_args(self):
+        """This method is to get args from actions and return optional args.
+
+        Returns:
+            A list of optional arguments.
+        """
+        argument_list = []
+        # The output of _get_optional_actions(): [['-t', '--test'], [--info]]
+        # return an argument list: ['-t', '--test', '--info']
+        for arg in self._get_optional_actions():
+            argument_list.extend(arg.option_strings)
+        return argument_list
+
+
+    EPILOG_TEXT = '''
+
+- - - - - - - - -
+IDENTIFYING TESTS
+- - - - - - - - -
+
+    The positional argument <tests> should be a reference to one or more
+    of the tests you'd like to run. Multiple tests can be run in one command by
+    separating test references with spaces.
+
+    Usage template: atest <reference_to_test_1> <reference_to_test_2>
+
+    A <reference_to_test> can be satisfied by the test's MODULE NAME,
+    MODULE:CLASS, CLASS NAME, TF INTEGRATION TEST, FILE PATH or PACKAGE NAME.
+    Explanations and examples of each follow.
+
+
+    < MODULE NAME >
+
+        Identifying a test by its module name will run the entire module. Input
+        the name as it appears in the LOCAL_MODULE or LOCAL_PACKAGE_NAME
+        variables in that test's Android.mk or Android.bp file.
+
+        Note: Use < TF INTEGRATION TEST > to run non-module tests integrated
+        directly into TradeFed.
+
+        Examples:
+            atest FrameworksServicesTests
+            atest CtsJankDeviceTestCases
+
+
+    < MODULE:CLASS >
+
+        Identifying a test by its class name will run just the tests in that
+        class and not the whole module. MODULE:CLASS is the preferred way to run
+        a single class. MODULE is the same as described above. CLASS is the
+        name of the test class in the .java file. It can either be the fully
+        qualified class name or just the basic name.
+
+        Examples:
+            atest FrameworksServicesTests:ScreenDecorWindowTests
+            atest FrameworksServicesTests:com.android.server.wm.ScreenDecorWindowTests
+            atest CtsJankDeviceTestCases:CtsDeviceJankUi
+
+
+    < CLASS NAME >
+
+        A single class can also be run by referencing the class name without
+        the module name.
+
+        Examples:
+            atest ScreenDecorWindowTests
+            atest CtsDeviceJankUi
+
+        However, this will take more time than the equivalent MODULE:CLASS
+        reference, so we suggest using a MODULE:CLASS reference whenever
+        possible. Examples below are ordered by performance from the fastest
+        to the slowest:
+
+        Examples:
+            atest FrameworksServicesTests:com.android.server.wm.ScreenDecorWindowTests
+            atest FrameworksServicesTests:ScreenDecorWindowTests
+            atest ScreenDecorWindowTests
+
+    < TF INTEGRATION TEST >
+
+        To run tests that are integrated directly into TradeFed (non-modules),
+        input the name as it appears in the output of the "tradefed.sh list
+        configs" cmd.
+
+        Examples:
+           atest example/reboot
+           atest native-benchmark
+
+
+    < FILE PATH >
+
+        Both module-based tests and integration-based tests can be run by
+        inputting the path to their test file or dir as appropriate. A single
+        class can also be run by inputting the path to the class's java file.
+        Both relative and absolute paths are supported.
+
+        Example - 2 ways to run the `CtsJankDeviceTestCases` module via path:
+        1. run module from android <repo root>:
+            atest cts/tests/jank/jank
+
+        2. from <android root>/cts/tests/jank:
+            atest .
+
+        Example - run a specific class within CtsJankDeviceTestCases module
+        from <android repo> root via path:
+           atest cts/tests/jank/src/android/jank/cts/ui/CtsDeviceJankUi.java
+
+        Example - run an integration test from <android repo> root via path:
+           atest tools/tradefederation/contrib/res/config/example/reboot.xml
+
+
+    < PACKAGE NAME >
+
+        Atest supports searching tests from package name as well.
+
+        Examples:
+           atest com.android.server.wm
+           atest android.jank.cts
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - -
+SPECIFYING INDIVIDUAL STEPS: BUILD, INSTALL OR RUN
+- - - - - - - - - - - - - - - - - - - - - - - - - -
+
+    The -b, -i and -t options allow you to specify which steps you want to run.
+    If none of those options are given, then all steps are run. If any of these
+    options are provided then only the listed steps are run.
+
+    Note: -i alone is not currently support and can only be included with -t.
+    Both -b and -t can be run alone.
+
+    Examples:
+        atest -b <test>    (just build targets)
+        atest -t <test>    (run tests only)
+        atest -it <test>   (install apk and run tests)
+        atest -bt <test>   (build targets, run tests, but skip installing apk)
+
+
+    Atest now has the ability to force a test to skip its cleanup/teardown step.
+    Many tests, e.g. CTS, cleanup the device after the test is run, so trying to
+    rerun your test with -t will fail without having the --disable-teardown
+    parameter. Use -d before -t to skip the test clean up step and test iteratively.
+
+        atest -d <test>    (disable installing apk and cleanning up device)
+        atest -t <test>
+
+    Note that -t disables both setup/install and teardown/cleanup of the
+    device. So you can continue to rerun your test with just
+
+        atest -t <test>
+
+    as many times as you want.
+
+
+- - - - - - - - - - - - -
+RUNNING SPECIFIC METHODS
+- - - - - - - - - - - - -
+
+    It is possible to run only specific methods within a test class. To run
+    only specific methods, identify the class in any of the ways supported for
+    identifying a class (MODULE:CLASS, FILE PATH, etc) and then append the
+    name of the method or method using the following template:
+
+      <reference_to_class>#<method1>
+
+    Multiple methods can be specified with commas:
+
+      <reference_to_class>#<method1>,<method2>,<method3>...
+
+    Examples:
+      atest com.android.server.wm.ScreenDecorWindowTests#testMultipleDecors
+
+      atest FrameworksServicesTests:ScreenDecorWindowTests#testFlagChange,testRemoval
+
+
+- - - - - - - - - - - - -
+RUNNING MULTIPLE CLASSES
+- - - - - - - - - - - - -
+
+    To run multiple classes, deliminate them with spaces just like you would
+    when running multiple tests.  Atest will handle building and running
+    classes in the most efficient way possible, so specifying a subset of
+    classes in a module will improve performance over running the whole module.
+
+
+    Examples:
+    - two classes in same module:
+      atest FrameworksServicesTests:ScreenDecorWindowTests FrameworksServicesTests:DimmerTests
+
+    - two classes, different modules:
+      atest FrameworksServicesTests:ScreenDecorWindowTests CtsJankDeviceTestCases:CtsDeviceJankUi
+
+
+- - - - - - - - - - -
+REGRESSION DETECTION
+- - - - - - - - - - -
+
+    Generate pre-patch or post-patch metrics without running regression detection:
+
+    Example:
+        atest <test> --generate-baseline <optional iter>
+        atest <test> --generate-new-metrics <optional iter>
+
+    Local regression detection can be run in three options:
+
+    1) Provide a folder containing baseline (pre-patch) metrics (generated
+       previously). Atest will run the tests n (default 5) iterations, generate
+       a new set of post-patch metrics, and compare those against existing metrics.
+
+    Example:
+        atest <test> --detect-regression </path/to/baseline> --generate-new-metrics <optional iter>
+
+    2) Provide a folder containing post-patch metrics (generated previously).
+       Atest will run the tests n (default 5) iterations, generate a new set of
+       pre-patch metrics, and compare those against those provided. Note: the
+       developer needs to revert the device/tests to pre-patch state to generate
+       baseline metrics.
+
+    Example:
+        atest <test> --detect-regression </path/to/new> --generate-baseline <optional iter>
+
+    3) Provide 2 folders containing both pre-patch and post-patch metrics. Atest
+       will run no tests but the regression detection algorithm.
+
+    Example:
+        atest --detect-regression </path/to/baseline> </path/to/new>
+
+
+- - - - - - - - - - - -
+TESTS IN TEST MAPPING
+- - - - - - - - - - - -
+
+    Atest can run tests in TEST_MAPPING files:
+
+    1) Run presubmit tests in TEST_MAPPING files in current and parent
+       directories. You can also specify a target directory.
+
+    Example:
+        atest  (run presubmit tests in TEST_MAPPING files in current and parent directories)
+        atest --test-mapping </path/to/project>
+               (run presubmit tests in TEST_MAPPING files in </path/to/project> and its parent directories)
+
+    2) Run a specified test group in TEST_MAPPING files.
+
+    Example:
+        atest :postsubmit
+              (run postsubmit tests in TEST_MAPPING files in current and parent directories)
+        atest :all
+              (Run tests from all groups in TEST_MAPPING files)
+        atest --test-mapping </path/to/project>:postsubmit
+              (run postsubmit tests in TEST_MAPPING files in </path/to/project> and its parent directories)
+
+    3) Run tests in TEST_MAPPING files including sub directories
+
+    By default, atest will only search for tests in TEST_MAPPING files in
+    current (or given directory) and its parent directories. If you want to run
+    tests in TEST_MAPPING files in the sub-directories, you can use option
+    --include-subdirs to force atest to include those tests too.
+
+    Example:
+        atest --include-subdirs [optional </path/to/project>:<test_group_name>]
+              (run presubmit tests in TEST_MAPPING files in current, sub and parent directories)
+    A path can be provided optionally if you want to search for tests in a give
+    directory, with optional test group name. By default, the test group is
+    presubmit.
+
+'''
diff --git a/atest/atest_arg_parser_unittest.py b/atest/atest_arg_parser_unittest.py
new file mode 100755
index 0000000..fd4c321
--- /dev/null
+++ b/atest/atest_arg_parser_unittest.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for atest_arg_parser."""
+
+import unittest
+
+import atest_arg_parser
+
+
+class AtestArgParserUnittests(unittest.TestCase):
+    """Unit tests for atest_arg_parser.py"""
+
+    def test_get_args(self):
+        """Test get_args(): flatten a nested list. """
+        parser = atest_arg_parser.AtestArgParser()
+        parser.add_argument('-t', '--test', help='Run the tests.')
+        parser.add_argument('-b', '--build', help='Run a build.')
+        parser.add_argument('--generate-baseline', help='Generate a baseline.')
+        test_args = ['-t', '--test',
+                     '-b', '--build',
+                     '--generate-baseline',
+                     '-h', '--help'].sort()
+        self.assertEqual(test_args, parser.get_args().sort())
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/atest_completion.sh b/atest/atest_completion.sh
new file mode 100644
index 0000000..286c62d
--- /dev/null
+++ b/atest/atest_completion.sh
@@ -0,0 +1,178 @@
+# Copyright 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.
+
+# Get testable module names from module_info.json.
+# Will return null if module_info.json doesn't exist.
+_fetch_testable_modules() {
+    [ -z $ANDROID_PRODUCT_OUT ] && { exit 0; }
+    $PYTHON - << END
+import hashlib
+import os
+import pickle
+import sys
+
+atest_dir = os.path.join(os.environ['ANDROID_BUILD_TOP'], 'tools/tradefederation/core/atest')
+sys.path.append(atest_dir)
+import module_info
+
+module_info_json = os.path.join(os.environ["ANDROID_PRODUCT_OUT"] ,"module-info.json")
+
+# TODO: This method should be implemented while making module-info.json.
+def get_serialised_filename(mod_info):
+    """Determine the serialised filename used for reading testable modules.
+
+    mod_info: the path of module-info.json.
+
+    Returns: a path string hashed with md5 of module-info.json.
+            /dev/shm/atest_e89e37a2e8e45be71567520b8579ffb8 (Linux)
+            /tmp/atest_e89e37a2e8e45be71567520b8579ffb8     (MacOSX)
+    """
+    serial_filename = "/tmp/atest_" if sys.platform == "darwin" else "/dev/shm/atest_"
+    with open(mod_info, 'r') as mod_info_obj:
+        serial_filename += hashlib.md5(mod_info_obj.read().encode('utf-8')).hexdigest()
+    return serial_filename
+
+# TODO: This method should be implemented while making module-info.json.
+def create_serialised_file(serial_file):
+    modules = module_info.ModuleInfo().get_testable_modules()
+    with open(serial_file, 'wb') as serial_file_obj:
+        pickle.dump(modules, serial_file_obj, protocol=2)
+
+if os.path.isfile(module_info_json):
+    latest_serial_file = get_serialised_filename(module_info_json)
+    # When module-info.json changes, recreate a serialisation file.
+    if not os.path.exists(latest_serial_file):
+        create_serialised_file(latest_serial_file)
+    else:
+        with open(latest_serial_file, 'rb') as serial_file_obj:
+            print("\n".join(pickle.load(serial_file_obj)))
+else:
+    print("")
+END
+}
+
+# This function invoke get_args() and return each item
+# of the list for tab completion candidates.
+_fetch_atest_args() {
+    [ -z $ANDROID_BUILD_TOP ] && { exit 0; }
+    $PYTHON - << END
+import os
+import sys
+
+atest_dir = os.path.join(os.environ['ANDROID_BUILD_TOP'], 'tools/tradefederation/core/atest')
+sys.path.append(atest_dir)
+
+import atest_arg_parser
+
+parser = atest_arg_parser.AtestArgParser()
+parser.add_atest_args()
+print("\n".join(parser.get_args()))
+END
+}
+
+# This function returns devices recognised by adb.
+_fetch_adb_devices() {
+    while read dev; do echo $dev | awk '{print $1}'; done < <(adb devices | egrep -v "^List|^$"||true)
+}
+
+# This function returns all paths contain TEST_MAPPING.
+_fetch_test_mapping_files() {
+    find -maxdepth 5 -type f -name TEST_MAPPING |sed 's/^.\///g'| xargs dirname 2>/dev/null
+}
+
+# The main tab completion function.
+_atest() {
+    # Not support completion on Darwin since the bash version of it
+    # is too old to fully support useful built-in commands/functions
+    # such as compopt, _get_comp_words_by_ref and __ltrim_colon_completions.
+    [[ "$(uname -s)" == "Darwin" ]] && return 0
+
+    local cur prev
+    COMPREPLY=()
+    cur="${COMP_WORDS[COMP_CWORD]}"
+    prev="${COMP_WORDS[COMP_CWORD-1]}"
+    _get_comp_words_by_ref -n : cur prev || true
+
+    case "$cur" in
+        -*)
+            COMPREPLY=($(compgen -W "$(_fetch_atest_args)" -- $cur))
+            ;;
+        */*)
+            ;;
+        *)
+            local candidate_args=$(ls; _fetch_testable_modules)
+            COMPREPLY=($(compgen -W "$candidate_args" -- $cur))
+            ;;
+    esac
+
+    case "$prev" in
+        --generate-baseline|--generate-new-metrics)
+            COMPREPLY=(5) ;;
+        --list-modules|-L)
+            # TODO: genetate the list automately when the API is available.
+            COMPREPLY=($(compgen -W "cts vts" -- $cur)) ;;
+        --serial|-s)
+            local adb_devices="$(_fetch_adb_devices)"
+            if [ -n "$adb_devices" ]; then
+                COMPREPLY=($(compgen -W "$(_fetch_adb_devices)" -- $cur))
+            else
+                # Don't complete files/dirs when there'is no devices.
+                compopt -o nospace
+                COMPREPLY=("")
+            fi ;;
+        --test-mapping|-p)
+            local mapping_files="$(_fetch_test_mapping_files)"
+            if [ -n "$mapping_files" ]; then
+                COMPREPLY=($(compgen -W "$mapping_files" -- $cur))
+            else
+                # Don't complete files/dirs when TEST_MAPPING wasn't found.
+                compopt -o nospace
+                COMPREPLY=("")
+            fi ;;
+    esac
+    __ltrim_colon_completions "$cur" "$prev" || true
+    return 0
+}
+
+function _atest_main() {
+    # Only use this in interactive mode.
+    [[ ! $- =~ 'i' ]] && return 0
+
+    # Use Py2 as the default interpreter. This script is aiming for being
+    # compatible with both Py2 and Py3.
+    PYTHON=
+    if [ -x "$(which python2)" ]; then
+        PYTHON=$(which python2)
+    elif [ -x "$(which python3)" ]; then
+        PYTHON=$(which python3)
+    else
+        PYTHON="/usr/bin/env python"
+    fi
+
+    # Complete file/dir name first by using option "nosort".
+    # BASH version <= 4.3 doesn't have nosort option.
+    # Note that nosort has no effect for zsh.
+    local _atest_comp_options="-o default -o nosort"
+    local _atest_executables=(atest atest-dev atest-src)
+    for exec in "${_atest_executables[*]}"; do
+        complete -F _atest $_atest_comp_options $exec 2>/dev/null || \
+        complete -F _atest -o default $exec
+    done
+
+    # Install atest-src for the convenience of debugging.
+    local atest_src="$(gettop)/tools/tradefederation/core/atest/atest.py"
+    [[ -f "$atest_src" ]] && alias atest-src="$atest_src"
+}
+
+_atest_main
diff --git a/atest/atest_decorator.py b/atest/atest_decorator.py
new file mode 100644
index 0000000..6f171df
--- /dev/null
+++ b/atest/atest_decorator.py
@@ -0,0 +1,33 @@
+# 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 decorator.
+"""
+
+def static_var(varname, value):
+    """Decorator to cache static variable.
+
+    Args:
+        varname: Variable name you want to use.
+        value: Variable value.
+
+    Returns: decorator function.
+    """
+
+    def fun_var_decorate(func):
+        """Set the static variable in a function."""
+        setattr(func, varname, value)
+        return func
+    return fun_var_decorate
diff --git a/atest/atest_enum.py b/atest/atest_enum.py
new file mode 100644
index 0000000..f4fb656
--- /dev/null
+++ b/atest/atest_enum.py
@@ -0,0 +1,21 @@
+# Copyright 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.
+
+"""
+Atest custom enum class.
+"""
+
+class AtestEnum(tuple):
+    """enum library isn't a Python 2.7 built-in, so roll our own."""
+    __getattr__ = tuple.index
diff --git a/atest/atest_error.py b/atest/atest_error.py
new file mode 100644
index 0000000..7ab8b5f
--- /dev/null
+++ b/atest/atest_error.py
@@ -0,0 +1,66 @@
+# Copyright 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.
+
+"""
+atest exceptions.
+"""
+
+
+class UnsupportedModuleTestError(Exception):
+    """Error raised when we find a module that we don't support."""
+
+class TestDiscoveryException(Exception):
+    """Base Exception for issues with test discovery."""
+
+class NoTestFoundError(TestDiscoveryException):
+    """Raised when no tests are found."""
+
+class TestWithNoModuleError(TestDiscoveryException):
+    """Raised when test files have no parent module directory."""
+
+class MissingPackageNameError(TestDiscoveryException):
+    """Raised when the test class java file does not contain a package name."""
+
+class TooManyMethodsError(TestDiscoveryException):
+    """Raised when input string contains more than one # character."""
+
+class MethodWithoutClassError(TestDiscoveryException):
+    """Raised when method is appended via # but no class file specified."""
+
+class UnknownTestRunnerError(Exception):
+    """Raised when an unknown test runner is specified."""
+
+class NoTestRunnerName(Exception):
+    """Raised when Test Runner class var NAME isn't defined."""
+
+class NoTestRunnerExecutable(Exception):
+    """Raised when Test Runner class var EXECUTABLE isn't defined."""
+
+class HostEnvCheckFailed(Exception):
+    """Raised when Test Runner's host env check fails."""
+
+class ShouldNeverBeCalledError(Exception):
+    """Raised when something is called when it shouldn't, used for testing."""
+
+class FatalIncludeError(TestDiscoveryException):
+    """Raised if expanding include tag fails."""
+
+class MissingCCTestCaseError(TestDiscoveryException):
+    """Raised when the cc file does not contain a test case class."""
+
+class XmlNotExistError(TestDiscoveryException):
+    """Raised when the xml file does not exist."""
+
+class DryRunVerificationError(Exception):
+    """Base Exception if verification fail."""
diff --git a/atest/atest_execution_info.py b/atest/atest_execution_info.py
new file mode 100644
index 0000000..1ef8634
--- /dev/null
+++ b/atest/atest_execution_info.py
@@ -0,0 +1,203 @@
+# 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 execution info generator.
+"""
+
+from __future__ import print_function
+
+import logging
+import json
+import os
+import sys
+
+import constants
+
+from metrics import metrics_utils
+
+_ARGS_KEY = 'args'
+_STATUS_PASSED_KEY = 'PASSED'
+_STATUS_FAILED_KEY = 'FAILED'
+_STATUS_IGNORED_KEY = 'IGNORED'
+_SUMMARY_KEY = 'summary'
+_TOTAL_SUMMARY_KEY = 'total_summary'
+_TEST_RUNNER_KEY = 'test_runner'
+_TEST_NAME_KEY = 'test_name'
+_TEST_TIME_KEY = 'test_time'
+_TEST_DETAILS_KEY = 'details'
+_TEST_RESULT_NAME = 'test_result'
+_EXIT_CODE_ATTR = 'EXIT_CODE'
+_MAIN_MODULE_KEY = '__main__'
+
+_SUMMARY_MAP_TEMPLATE = {_STATUS_PASSED_KEY : 0,
+                         _STATUS_FAILED_KEY : 0,
+                         _STATUS_IGNORED_KEY : 0,}
+
+PREPARE_END_TIME = None
+
+
+def preparation_time(start_time):
+    """Return the preparation time.
+
+    Args:
+        start_time: The time.
+
+    Returns:
+        The preparation time if PREPARE_END_TIME is set, None otherwise.
+    """
+    return PREPARE_END_TIME - start_time if PREPARE_END_TIME else None
+
+
+class AtestExecutionInfo(object):
+    """Class that stores the whole test progress information in JSON format.
+
+    ----
+    For example, running command
+        atest hello_world_test HelloWorldTest
+
+    will result in storing the execution detail in JSON:
+    {
+      "args": "hello_world_test HelloWorldTest",
+      "test_runner": {
+          "AtestTradefedTestRunner": {
+              "hello_world_test": {
+                  "FAILED": [
+                      {"test_time": "(5ms)",
+                       "details": "Hello, Wor...",
+                       "test_name": "HelloWorldTest#PrintHelloWorld"}
+                      ],
+                  "summary": {"FAILED": 1, "PASSED": 0, "IGNORED": 0}
+              },
+              "HelloWorldTests": {
+                  "PASSED": [
+                      {"test_time": "(27ms)",
+                       "details": null,
+                       "test_name": "...HelloWorldTest#testHalloWelt"},
+                      {"test_time": "(1ms)",
+                       "details": null,
+                       "test_name": "....HelloWorldTest#testHelloWorld"}
+                      ],
+                  "summary": {"FAILED": 0, "PASSED": 2, "IGNORED": 0}
+              }
+          }
+      },
+      "total_summary": {"FAILED": 1, "PASSED": 2, "IGNORED": 0}
+    }
+    """
+
+    result_reporters = []
+
+    def __init__(self, args, work_dir):
+        """Initialise an AtestExecutionInfo instance.
+
+        Args:
+            args: Command line parameters.
+            work_dir : The directory for saving information.
+
+        Returns:
+               A json format string.
+        """
+        self.args = args
+        self.work_dir = work_dir
+        self.result_file = None
+
+    def __enter__(self):
+        """Create and return information file object."""
+        full_file_name = os.path.join(self.work_dir, _TEST_RESULT_NAME)
+        try:
+            self.result_file = open(full_file_name, 'w')
+        except IOError:
+            logging.error('Cannot open file %s', full_file_name)
+        return self.result_file
+
+    def __exit__(self, exit_type, value, traceback):
+        """Write execution information and close information file."""
+        if self.result_file:
+            self.result_file.write(AtestExecutionInfo.
+                                   _generate_execution_detail(self.args))
+            self.result_file.close()
+        main_module = sys.modules.get(_MAIN_MODULE_KEY)
+        main_exit_code = getattr(main_module, _EXIT_CODE_ATTR,
+                                 constants.EXIT_CODE_ERROR)
+        if main_exit_code == constants.EXIT_CODE_SUCCESS:
+            metrics_utils.send_exit_event(main_exit_code)
+        else:
+            metrics_utils.handle_exc_and_send_exit_event(main_exit_code)
+
+    @staticmethod
+    def _generate_execution_detail(args):
+        """Generate execution detail.
+
+        Args:
+            args: Command line parameters that you want to save.
+
+        Returns:
+            A json format string.
+        """
+        info_dict = {_ARGS_KEY: ' '.join(args)}
+        try:
+            AtestExecutionInfo._arrange_test_result(
+                info_dict,
+                AtestExecutionInfo.result_reporters)
+            return json.dumps(info_dict)
+        except ValueError as err:
+            logging.warn('Parsing test result failed due to : %s', err)
+
+    @staticmethod
+    def _arrange_test_result(info_dict, reporters):
+        """Append test result information in given dict.
+
+        Arrange test information to below
+        "test_runner": {
+            "test runner name": {
+                "test name": {
+                    "FAILED": [
+                        {"test time": "",
+                         "details": "",
+                         "test name": ""}
+                    ],
+                "summary": {"FAILED": 0, "PASSED": 0, "IGNORED": 0}
+                },
+            },
+        "total_summary": {"FAILED": 0, "PASSED": 0, "IGNORED": 0}
+
+        Args:
+            info_dict: A dict you want to add result information in.
+            reporters: A list of result_reporter.
+
+        Returns:
+            A dict contains test result information data.
+        """
+        info_dict[_TEST_RUNNER_KEY] = {}
+        for reporter in reporters:
+            for test in reporter.all_test_results:
+                runner = info_dict[_TEST_RUNNER_KEY].setdefault(test.runner_name, {})
+                group = runner.setdefault(test.group_name, {})
+                result_dict = {_TEST_NAME_KEY : test.test_name,
+                               _TEST_TIME_KEY : test.test_time,
+                               _TEST_DETAILS_KEY : test.details}
+                group.setdefault(test.status, []).append(result_dict)
+
+        total_test_group_summary = _SUMMARY_MAP_TEMPLATE.copy()
+        for runner in info_dict[_TEST_RUNNER_KEY]:
+            for group in info_dict[_TEST_RUNNER_KEY][runner]:
+                group_summary = _SUMMARY_MAP_TEMPLATE.copy()
+                for status in info_dict[_TEST_RUNNER_KEY][runner][group]:
+                    count = len(info_dict[_TEST_RUNNER_KEY][runner][group][status])
+                    if _SUMMARY_MAP_TEMPLATE.has_key(status):
+                        group_summary[status] = count
+                        total_test_group_summary[status] += count
+                info_dict[_TEST_RUNNER_KEY][runner][group][_SUMMARY_KEY] = group_summary
+        info_dict[_TOTAL_SUMMARY_KEY] = total_test_group_summary
+        return info_dict
diff --git a/atest/atest_execution_info_unittest.py b/atest/atest_execution_info_unittest.py
new file mode 100755
index 0000000..f638f82
--- /dev/null
+++ b/atest/atest_execution_info_unittest.py
@@ -0,0 +1,164 @@
+#!/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_execution_info."""
+
+import time
+import unittest
+
+from test_runners import test_runner_base
+import atest_execution_info as aei
+import result_reporter
+
+RESULT_TEST_TEMPLATE = test_runner_base.TestResult(
+    runner_name='someRunner',
+    group_name='someModule',
+    test_name='someClassName#sostName',
+    status=test_runner_base.PASSED_STATUS,
+    details=None,
+    test_count=1,
+    test_time='(10ms)',
+    runner_total=None,
+    group_total=2,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+# pylint: disable=protected-access
+class AtestRunInfoUnittests(unittest.TestCase):
+    """Unit tests for atest_execution_info.py"""
+
+    def test_arrange_test_result_one_module(self):
+        """Test _arrange_test_result method with only one module."""
+        pass_1 = self._create_test_result(status=test_runner_base.PASSED_STATUS)
+        pass_2 = self._create_test_result(status=test_runner_base.PASSED_STATUS)
+        pass_3 = self._create_test_result(status=test_runner_base.PASSED_STATUS)
+        fail_1 = self._create_test_result(status=test_runner_base.FAILED_STATUS)
+        fail_2 = self._create_test_result(status=test_runner_base.FAILED_STATUS)
+        ignore_1 = self._create_test_result(status=test_runner_base.IGNORED_STATUS)
+        reporter_1 = result_reporter.ResultReporter()
+        reporter_1.all_test_results.extend([pass_1, pass_2, pass_3])
+        reporter_2 = result_reporter.ResultReporter()
+        reporter_2.all_test_results.extend([fail_1, fail_2, ignore_1])
+        info_dict = {}
+        aei.AtestExecutionInfo._arrange_test_result(info_dict, [reporter_1, reporter_2])
+        expect_summary = {aei._STATUS_IGNORED_KEY : 1,
+                          aei._STATUS_FAILED_KEY : 2,
+                          aei._STATUS_PASSED_KEY : 3}
+        self.assertEqual(expect_summary, info_dict[aei._TOTAL_SUMMARY_KEY])
+
+    def test_arrange_test_result_multi_module(self):
+        """Test _arrange_test_result method with multi module."""
+        group_a_pass_1 = self._create_test_result(group_name='grpup_a',
+                                                  status=test_runner_base.PASSED_STATUS)
+        group_b_pass_1 = self._create_test_result(group_name='grpup_b',
+                                                  status=test_runner_base.PASSED_STATUS)
+        group_c_pass_1 = self._create_test_result(group_name='grpup_c',
+                                                  status=test_runner_base.PASSED_STATUS)
+        group_b_fail_1 = self._create_test_result(group_name='grpup_b',
+                                                  status=test_runner_base.FAILED_STATUS)
+        group_c_fail_1 = self._create_test_result(group_name='grpup_c',
+                                                  status=test_runner_base.FAILED_STATUS)
+        group_c_ignore_1 = self._create_test_result(group_name='grpup_c',
+                                                    status=test_runner_base.IGNORED_STATUS)
+        reporter_1 = result_reporter.ResultReporter()
+        reporter_1.all_test_results.extend([group_a_pass_1, group_b_pass_1, group_c_pass_1])
+        reporter_2 = result_reporter.ResultReporter()
+        reporter_2.all_test_results.extend([group_b_fail_1, group_c_fail_1, group_c_ignore_1])
+
+        info_dict = {}
+        aei.AtestExecutionInfo._arrange_test_result(info_dict, [reporter_1, reporter_2])
+        expect_group_a_summary = {aei._STATUS_IGNORED_KEY : 0,
+                                  aei._STATUS_FAILED_KEY : 0,
+                                  aei._STATUS_PASSED_KEY : 1}
+        self.assertEqual(
+            expect_group_a_summary,
+            info_dict[aei._TEST_RUNNER_KEY]['someRunner']['grpup_a'][aei._SUMMARY_KEY])
+
+        expect_group_b_summary = {aei._STATUS_IGNORED_KEY : 0,
+                                  aei._STATUS_FAILED_KEY : 1,
+                                  aei._STATUS_PASSED_KEY : 1}
+        self.assertEqual(
+            expect_group_b_summary,
+            info_dict[aei._TEST_RUNNER_KEY]['someRunner']['grpup_b'][aei._SUMMARY_KEY])
+
+        expect_group_c_summary = {aei._STATUS_IGNORED_KEY : 1,
+                                  aei._STATUS_FAILED_KEY : 1,
+                                  aei._STATUS_PASSED_KEY : 1}
+        self.assertEqual(
+            expect_group_c_summary,
+            info_dict[aei._TEST_RUNNER_KEY]['someRunner']['grpup_c'][aei._SUMMARY_KEY])
+
+        expect_total_summary = {aei._STATUS_IGNORED_KEY : 1,
+                                aei._STATUS_FAILED_KEY : 2,
+                                aei._STATUS_PASSED_KEY : 3}
+        self.assertEqual(expect_total_summary, info_dict[aei._TOTAL_SUMMARY_KEY])
+
+    def test_preparation_time(self):
+        """Test preparation_time method."""
+        start_time = time.time()
+        aei.PREPARE_END_TIME = None
+        self.assertTrue(aei.preparation_time(start_time) is None)
+        aei.PREPARE_END_TIME = time.time()
+        self.assertFalse(aei.preparation_time(start_time) is None)
+
+    def test_arrange_test_result_multi_runner(self):
+        """Test _arrange_test_result method with multi runner."""
+        runner_a_pass_1 = self._create_test_result(runner_name='runner_a',
+                                                   status=test_runner_base.PASSED_STATUS)
+        runner_a_pass_2 = self._create_test_result(runner_name='runner_a',
+                                                   status=test_runner_base.PASSED_STATUS)
+        runner_a_pass_3 = self._create_test_result(runner_name='runner_a',
+                                                   status=test_runner_base.PASSED_STATUS)
+        runner_b_fail_1 = self._create_test_result(runner_name='runner_b',
+                                                   status=test_runner_base.FAILED_STATUS)
+        runner_b_fail_2 = self._create_test_result(runner_name='runner_b',
+                                                   status=test_runner_base.FAILED_STATUS)
+        runner_b_ignore_1 = self._create_test_result(runner_name='runner_b',
+                                                     status=test_runner_base.IGNORED_STATUS)
+
+        reporter_1 = result_reporter.ResultReporter()
+        reporter_1.all_test_results.extend([runner_a_pass_1, runner_a_pass_2, runner_a_pass_3])
+        reporter_2 = result_reporter.ResultReporter()
+        reporter_2.all_test_results.extend([runner_b_fail_1, runner_b_fail_2, runner_b_ignore_1])
+        info_dict = {}
+        aei.AtestExecutionInfo._arrange_test_result(info_dict, [reporter_1, reporter_2])
+        expect_group_a_summary = {aei._STATUS_IGNORED_KEY : 0,
+                                  aei._STATUS_FAILED_KEY : 0,
+                                  aei._STATUS_PASSED_KEY : 3}
+        self.assertEqual(
+            expect_group_a_summary,
+            info_dict[aei._TEST_RUNNER_KEY]['runner_a']['someModule'][aei._SUMMARY_KEY])
+
+        expect_group_b_summary = {aei._STATUS_IGNORED_KEY : 1,
+                                  aei._STATUS_FAILED_KEY : 2,
+                                  aei._STATUS_PASSED_KEY : 0}
+        self.assertEqual(
+            expect_group_b_summary,
+            info_dict[aei._TEST_RUNNER_KEY]['runner_b']['someModule'][aei._SUMMARY_KEY])
+
+        expect_total_summary = {aei._STATUS_IGNORED_KEY : 1,
+                                aei._STATUS_FAILED_KEY : 2,
+                                aei._STATUS_PASSED_KEY : 3}
+        self.assertEqual(expect_total_summary, info_dict[aei._TOTAL_SUMMARY_KEY])
+
+    def _create_test_result(self, **kwargs):
+        """A Helper to create TestResult"""
+        test_info = test_runner_base.TestResult(**RESULT_TEST_TEMPLATE._asdict())
+        return test_info._replace(**kwargs)
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/atest/atest_integration_tests.py b/atest/atest_integration_tests.py
new file mode 100755
index 0000000..3ed1247
--- /dev/null
+++ b/atest/atest_integration_tests.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""
+ATest Integration Test Class.
+
+The purpose is to prevent potential side-effects from breaking ATest at the
+early stage while landing CLs with potential side-effects.
+
+It forks a subprocess with ATest commands to validate if it can pass all the
+finding, running logic of the python code, and waiting for TF to exit properly.
+    - When running with ROBOLECTRIC tests, it runs without TF, and will exit
+    the subprocess with the message "All tests passed"
+    - If FAIL, it means something breaks ATest unexpectedly!
+"""
+
+from __future__ import print_function
+
+import os
+import subprocess
+import sys
+import tempfile
+import time
+import unittest
+
+_TEST_RUN_DIR_PREFIX = 'atest_integration_tests_%s_'
+_LOG_FILE = 'integration_tests.log'
+_FAILED_LINE_LIMIT = 50
+_INTEGRATION_TESTS = 'INTEGRATION_TESTS'
+
+
+class ATestIntegrationTest(unittest.TestCase):
+    """ATest Integration Test Class."""
+    NAME = 'ATestIntegrationTest'
+    EXECUTABLE = 'atest'
+    OPTIONS = ''
+    _RUN_CMD = '{exe} {options} {test}'
+    _PASSED_CRITERIA = ['will be rescheduled', 'All tests passed']
+
+    def setUp(self):
+        """Set up stuff for testing."""
+        self.full_env_vars = os.environ.copy()
+        self.test_passed = False
+        self.log = []
+
+    def run_test(self, testcase):
+        """Create a subprocess to execute the test command.
+
+        Strategy:
+            Fork a subprocess to wait for TF exit properly, and log the error
+            if the exit code isn't 0.
+
+        Args:
+            testcase: A string of testcase name.
+        """
+        run_cmd_dict = {'exe': self.EXECUTABLE, 'options': self.OPTIONS,
+                        'test': testcase}
+        run_command = self._RUN_CMD.format(**run_cmd_dict)
+        try:
+            subprocess.check_output(run_command,
+                                    stderr=subprocess.PIPE,
+                                    env=self.full_env_vars,
+                                    shell=True)
+        except subprocess.CalledProcessError as e:
+            self.log.append(e.output)
+            return False
+        return True
+
+    def get_failed_log(self):
+        """Get a trimmed failed log.
+
+        Strategy:
+            In order not to show the unnecessary log such as build log,
+            it's better to get a trimmed failed log that contains the
+            most important information.
+
+        Returns:
+            A trimmed failed log.
+        """
+        failed_log = '\n'.join(filter(None, self.log[-_FAILED_LINE_LIMIT:]))
+        return failed_log
+
+
+def create_test_method(testcase, log_path):
+    """Create a test method according to the testcase.
+
+    Args:
+        testcase: A testcase name.
+        log_path: A file path for storing the test result.
+
+    Returns:
+        A created test method, and a test function name.
+    """
+    test_function_name = 'test_%s' % testcase.replace(' ', '_')
+    # pylint: disable=missing-docstring
+    def template_test_method(self):
+        self.test_passed = self.run_test(testcase)
+        open(log_path, 'a').write('\n'.join(self.log))
+        failed_message = 'Running command: %s failed.\n' % testcase
+        failed_message += '' if self.test_passed else self.get_failed_log()
+        self.assertTrue(self.test_passed, failed_message)
+    return test_function_name, template_test_method
+
+
+def create_test_run_dir():
+    """Create the test run directory in tmp.
+
+    Returns:
+        A string of the directory path.
+    """
+    utc_epoch_time = int(time.time())
+    prefix = _TEST_RUN_DIR_PREFIX % utc_epoch_time
+    return tempfile.mkdtemp(prefix=prefix)
+
+
+if __name__ == '__main__':
+    # TODO(b/129029189) Implement detail comparison check for dry-run mode.
+    ARGS = ' '.join(sys.argv[1:])
+    if ARGS:
+        ATestIntegrationTest.OPTIONS = ARGS
+    TEST_PLANS = os.path.join(os.path.dirname(__file__), _INTEGRATION_TESTS)
+    try:
+        LOG_PATH = os.path.join(create_test_run_dir(), _LOG_FILE)
+        with open(TEST_PLANS) as test_plans:
+            for test in test_plans:
+                # Skip test when the line startswith #.
+                if not test.strip() or test.strip().startswith('#'):
+                    continue
+                test_func_name, test_func = create_test_method(
+                    test.strip(), LOG_PATH)
+                setattr(ATestIntegrationTest, test_func_name, test_func)
+        SUITE = unittest.TestLoader().loadTestsFromTestCase(ATestIntegrationTest)
+        RESULTS = unittest.TextTestRunner(verbosity=2).run(SUITE)
+    finally:
+        if RESULTS.failures:
+            print('Full test log is saved to %s' % LOG_PATH)
+        else:
+            os.remove(LOG_PATH)
diff --git a/atest/atest_integration_tests.xml b/atest/atest_integration_tests.xml
new file mode 100644
index 0000000..dd8ee82
--- /dev/null
+++ b/atest/atest_integration_tests.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config to run atest integration tests">
+    <option name="test-suite-tag" value="atest_integration_tests" />
+
+    <test class="com.android.tradefed.testtype.python.PythonBinaryHostTest" >
+        <option name="par-file-name" value="atest_integration_tests" />
+        <option name="test-timeout" value="120m" />
+    </test>
+</configuration>
diff --git a/atest/atest_metrics.py b/atest/atest_metrics.py
new file mode 100755
index 0000000..d2ac3ad
--- /dev/null
+++ b/atest/atest_metrics.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Simple Metrics Functions"""
+
+import constants
+import asuite_metrics
+
+
+#pylint: disable=broad-except
+def log_start_event():
+    """Log that atest started."""
+    asuite_metrics.log_event(constants.METRICS_URL)
diff --git a/atest/atest_run_unittests.py b/atest/atest_run_unittests.py
new file mode 100755
index 0000000..f23c59d
--- /dev/null
+++ b/atest/atest_run_unittests.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+"""Main entrypoint for all of atest's unittest."""
+
+import logging
+import os
+import sys
+import unittest
+from importlib import import_module
+
+# Setup logging to be silent so unittests can pass through TF.
+logging.disable(logging.ERROR)
+
+def get_test_modules():
+    """Returns a list of testable modules.
+
+    Finds all the test files (*_unittest.py) and get their relative
+    path (internal/lib/utils_test.py) and translate it to an import path and
+    strip the py ext (internal.lib.utils_test).
+
+    Returns:
+        List of strings (the testable module import path).
+    """
+    testable_modules = []
+    base_path = os.path.dirname(os.path.realpath(__file__))
+
+    for dirpath, _, files in os.walk(base_path):
+        for f in files:
+            if f.endswith("_unittest.py"):
+                # Now transform it into a relative import path.
+                full_file_path = os.path.join(dirpath, f)
+                rel_file_path = os.path.relpath(full_file_path, base_path)
+                rel_file_path, _ = os.path.splitext(rel_file_path)
+                rel_file_path = rel_file_path.replace(os.sep, ".")
+                testable_modules.append(rel_file_path)
+
+    return testable_modules
+
+def main(_):
+    """Main unittest entry.
+
+    Args:
+        argv: A list of system arguments. (unused)
+
+    Returns:
+        0 if success. None-zero if fails.
+    """
+    test_modules = get_test_modules()
+    for mod in test_modules:
+        import_module(mod)
+
+    loader = unittest.defaultTestLoader
+    test_suite = loader.loadTestsFromNames(test_modules)
+    runner = unittest.TextTestRunner(verbosity=2)
+    result = runner.run(test_suite)
+    sys.exit(not result.wasSuccessful())
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
diff --git a/atest/atest_unittest.py b/atest/atest_unittest.py
new file mode 100755
index 0000000..5600d75
--- /dev/null
+++ b/atest/atest_unittest.py
@@ -0,0 +1,300 @@
+#!/usr/bin/env python
+#
+# Copyright 2017, 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 atest."""
+
+import datetime
+import os
+import sys
+import tempfile
+import unittest
+import mock
+
+import atest
+import constants
+import module_info
+
+from metrics import metrics_utils
+from test_finders import test_info
+
+if sys.version_info[0] == 2:
+    from StringIO import StringIO
+else:
+    from io import StringIO
+
+#pylint: disable=protected-access
+class AtestUnittests(unittest.TestCase):
+    """Unit tests for atest.py"""
+
+    @mock.patch('os.environ.get', return_value=None)
+    def test_missing_environment_variables_uninitialized(self, _):
+        """Test _has_environment_variables when no env vars."""
+        self.assertTrue(atest._missing_environment_variables())
+
+    @mock.patch('os.environ.get', return_value='out/testcases/')
+    def test_missing_environment_variables_initialized(self, _):
+        """Test _has_environment_variables when env vars."""
+        self.assertFalse(atest._missing_environment_variables())
+
+    def test_parse_args(self):
+        """Test _parse_args parses command line args."""
+        test_one = 'test_name_one'
+        test_two = 'test_name_two'
+        custom_arg = '--custom_arg'
+        custom_arg_val = 'custom_arg_val'
+        pos_custom_arg = 'pos_custom_arg'
+
+        # Test out test and custom args are properly retrieved.
+        args = [test_one, test_two, '--', custom_arg, custom_arg_val]
+        parsed_args = atest._parse_args(args)
+        self.assertEqual(parsed_args.tests, [test_one, test_two])
+        self.assertEqual(parsed_args.custom_args, [custom_arg, custom_arg_val])
+
+        # Test out custom positional args with no test args.
+        args = ['--', pos_custom_arg, custom_arg_val]
+        parsed_args = atest._parse_args(args)
+        self.assertEqual(parsed_args.tests, [])
+        self.assertEqual(parsed_args.custom_args, [pos_custom_arg,
+                                                   custom_arg_val])
+
+    def test_has_valid_test_mapping_args(self):
+        """Test _has_valid_test_mapping_args mehod."""
+        # Test test mapping related args are not mixed with incompatible args.
+        options_no_tm_support = [
+            ('--generate-baseline', '5'),
+            ('--detect-regression', 'path'),
+            ('--generate-new-metrics', '5')
+        ]
+        tm_options = [
+            '--test-mapping',
+            '--include-subdirs'
+        ]
+
+        for tm_option in tm_options:
+            for no_tm_option, no_tm_option_value in options_no_tm_support:
+                args = [tm_option, no_tm_option]
+                if no_tm_option_value != None:
+                    args.append(no_tm_option_value)
+                parsed_args = atest._parse_args(args)
+                self.assertFalse(
+                    atest._has_valid_test_mapping_args(parsed_args),
+                    'Failed to validate: %s' % args)
+
+    @mock.patch('json.load', return_value={})
+    @mock.patch('__builtin__.open', new_callable=mock.mock_open)
+    @mock.patch('os.path.isfile', return_value=True)
+    @mock.patch('atest_utils._has_colors', return_value=True)
+    @mock.patch.object(module_info.ModuleInfo, 'get_module_info',)
+    def test_print_module_info_from_module_name(self, mock_get_module_info,
+                                                _mock_has_colors, _isfile,
+                                                _open, _json):
+        """Test _print_module_info_from_module_name mehod."""
+        mod_one_name = 'mod1'
+        mod_one_path = ['src/path/mod1']
+        mod_one_installed = ['installed/path/mod1']
+        mod_one_suites = ['device_test_mod1', 'native_test_mod1']
+        mod_one = {constants.MODULE_NAME: mod_one_name,
+                   constants.MODULE_PATH: mod_one_path,
+                   constants.MODULE_INSTALLED: mod_one_installed,
+                   constants.MODULE_COMPATIBILITY_SUITES: mod_one_suites}
+
+        # Case 1: The testing_module('mod_one') can be found in module_info.
+        mock_get_module_info.return_value = mod_one
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        mod_info = module_info.ModuleInfo()
+        # Check return value = True, since 'mod_one' can be found.
+        self.assertTrue(
+            atest._print_module_info_from_module_name(mod_info, mod_one_name))
+        # Assign sys.stdout back to default.
+        sys.stdout = sys.__stdout__
+        correct_output = ('\x1b[1;32mmod1\x1b[0m\n'
+                          '\x1b[1;36m\tCompatibility suite\x1b[0m\n'
+                          '\t\tdevice_test_mod1\n'
+                          '\t\tnative_test_mod1\n'
+                          '\x1b[1;36m\tSource code path\x1b[0m\n'
+                          '\t\tsrc/path/mod1\n'
+                          '\x1b[1;36m\tInstalled path\x1b[0m\n'
+                          '\t\tinstalled/path/mod1\n')
+        # Check the function correctly printed module_info in color to stdout
+        self.assertEqual(capture_output.getvalue(), correct_output)
+
+        # Case 2: The testing_module('mod_one') can NOT be found in module_info.
+        mock_get_module_info.return_value = None
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        # Check return value = False, since 'mod_one' can NOT be found.
+        self.assertFalse(
+            atest._print_module_info_from_module_name(mod_info, mod_one_name))
+        # Assign sys.stdout back to default.
+        sys.stdout = sys.__stdout__
+        null_output = ''
+        # Check if no module_info, then nothing printed to screen.
+        self.assertEqual(capture_output.getvalue(), null_output)
+
+    @mock.patch('json.load', return_value={})
+    @mock.patch('__builtin__.open', new_callable=mock.mock_open)
+    @mock.patch('os.path.isfile', return_value=True)
+    @mock.patch('atest_utils._has_colors', return_value=True)
+    @mock.patch.object(module_info.ModuleInfo, 'get_module_info',)
+    def test_print_test_info(self, mock_get_module_info, _mock_has_colors,
+                             _isfile, _open, _json):
+        """Test _print_test_info mehod."""
+        mod_one_name = 'mod1'
+        mod_one = {constants.MODULE_NAME: mod_one_name,
+                   constants.MODULE_PATH: ['path/mod1'],
+                   constants.MODULE_INSTALLED: ['installed/mod1'],
+                   constants.MODULE_COMPATIBILITY_SUITES: ['suite_mod1']}
+        mod_two_name = 'mod2'
+        mod_two = {constants.MODULE_NAME: mod_two_name,
+                   constants.MODULE_PATH: ['path/mod2'],
+                   constants.MODULE_INSTALLED: ['installed/mod2'],
+                   constants.MODULE_COMPATIBILITY_SUITES: ['suite_mod2']}
+        mod_three_name = 'mod3'
+        mod_three = {constants.MODULE_NAME: mod_two_name,
+                     constants.MODULE_PATH: ['path/mod3'],
+                     constants.MODULE_INSTALLED: ['installed/mod3'],
+                     constants.MODULE_COMPATIBILITY_SUITES: ['suite_mod3']}
+        test_name = mod_one_name
+        build_targets = set([mod_one_name, mod_two_name, mod_three_name])
+        t_info = test_info.TestInfo(test_name, 'mock_runner', build_targets)
+        test_infos = set([t_info])
+
+        # The _print_test_info() will print the module_info of the test_info's
+        # test_name first. Then, print its related build targets. If the build
+        # target be printed before(e.g. build_target == test_info's test_name),
+        # it will skip it and print the next build_target.
+        # Since the build_targets of test_info are mod_one, mod_two, and
+        # mod_three, it will print mod_one first, then mod_two, and mod_three.
+        #
+        # _print_test_info() calls _print_module_info_from_module_name() to
+        # print the module_info. And _print_module_info_from_module_name()
+        # calls get_module_info() to get the module_info. So we can mock
+        # get_module_info() to achieve that.
+        mock_get_module_info.side_effect = [mod_one, mod_two, mod_three]
+
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        mod_info = module_info.ModuleInfo()
+        atest._print_test_info(mod_info, test_infos)
+        # Assign sys.stdout back to default.
+        sys.stdout = sys.__stdout__
+        correct_output = ('\x1b[1;32mmod1\x1b[0m\n'
+                          '\x1b[1;36m\tCompatibility suite\x1b[0m\n'
+                          '\t\tsuite_mod1\n'
+                          '\x1b[1;36m\tSource code path\x1b[0m\n'
+                          '\t\tpath/mod1\n'
+                          '\x1b[1;36m\tInstalled path\x1b[0m\n'
+                          '\t\tinstalled/mod1\n'
+                          '\x1b[1;35m\tRelated build targets\x1b[0m\n'
+                          '\t\tmod1, mod2, mod3\n'
+                          '\x1b[1;32mmod2\x1b[0m\n'
+                          '\x1b[1;36m\tCompatibility suite\x1b[0m\n'
+                          '\t\tsuite_mod2\n'
+                          '\x1b[1;36m\tSource code path\x1b[0m\n'
+                          '\t\tpath/mod2\n'
+                          '\x1b[1;36m\tInstalled path\x1b[0m\n'
+                          '\t\tinstalled/mod2\n'
+                          '\x1b[1;32mmod3\x1b[0m\n'
+                          '\x1b[1;36m\tCompatibility suite\x1b[0m\n'
+                          '\t\tsuite_mod3\n'
+                          '\x1b[1;36m\tSource code path\x1b[0m\n'
+                          '\t\tpath/mod3\n'
+                          '\x1b[1;36m\tInstalled path\x1b[0m\n'
+                          '\t\tinstalled/mod3\n'
+                          '\x1b[1;37m\x1b[0m\n')
+        self.assertEqual(capture_output.getvalue(), correct_output)
+
+    @mock.patch.object(metrics_utils, 'send_exit_event')
+    def test_validate_exec_mode(self, _send_exit):
+        """Test _validate_exec_mode."""
+        args = []
+        parsed_args = atest._parse_args(args)
+        no_install_test_info = test_info.TestInfo(
+            'mod', '', set(), data={}, module_class=["JAVA_LIBRARIES"],
+            install_locations=set(['device']))
+        host_test_info = test_info.TestInfo(
+            'mod', '', set(), data={}, module_class=["NATIVE_TESTS"],
+            install_locations=set(['host']))
+        device_test_info = test_info.TestInfo(
+            'mod', '', set(), data={}, module_class=["NATIVE_TESTS"],
+            install_locations=set(['device']))
+        both_test_info = test_info.TestInfo(
+            'mod', '', set(), data={}, module_class=["NATIVE_TESTS"],
+            install_locations=set(['host', 'device']))
+
+        # $atest <Both-support>
+        test_infos = [host_test_info]
+        atest._validate_exec_mode(parsed_args, test_infos)
+        self.assertFalse(parsed_args.host)
+
+        # $atest <Both-support> with host_tests set to True
+        parsed_args = atest._parse_args([])
+        test_infos = [host_test_info]
+        atest._validate_exec_mode(parsed_args, test_infos, host_tests=True)
+        # Make sure the host option is not set.
+        self.assertFalse(parsed_args.host)
+
+        # $atest <Both-support> with host_tests set to False
+        parsed_args = atest._parse_args([])
+        test_infos = [host_test_info]
+        atest._validate_exec_mode(parsed_args, test_infos, host_tests=False)
+        self.assertFalse(parsed_args.host)
+
+        # $atest <device-only> with host_tests set to False
+        parsed_args = atest._parse_args([])
+        test_infos = [device_test_info]
+        atest._validate_exec_mode(parsed_args, test_infos, host_tests=False)
+        # Make sure the host option is not set.
+        self.assertFalse(parsed_args.host)
+
+        # $atest <device-only> with host_tests set to True
+        parsed_args = atest._parse_args([])
+        test_infos = [device_test_info]
+        self.assertRaises(SystemExit, atest._validate_exec_mode,
+                          parsed_args, test_infos, host_tests=True)
+
+        # $atest <Both-support>
+        parsed_args = atest._parse_args([])
+        test_infos = [both_test_info]
+        atest._validate_exec_mode(parsed_args, test_infos)
+        self.assertFalse(parsed_args.host)
+
+        # $atest <no_install_test_info>
+        parsed_args = atest._parse_args([])
+        test_infos = [no_install_test_info]
+        atest._validate_exec_mode(parsed_args, test_infos)
+        self.assertFalse(parsed_args.host)
+
+    def test_make_test_run_dir(self):
+        """Test make_test_run_dir."""
+        tmp_dir = tempfile.mkdtemp()
+        constants.ATEST_RESULT_ROOT = tmp_dir
+        data_time = None
+        try:
+            word_dir = atest.make_test_run_dir()
+            folder_name = os.path.basename(word_dir)
+            data_time = datetime.datetime.strptime('_'.join(folder_name.split('_')[:-1]),
+                                                   atest.TEST_RUN_DIR_PREFIX)
+        except ValueError:
+            pass
+        finally:
+            reload(constants)
+        self.assertTrue(data_time)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/atest_unittests.xml b/atest/atest_unittests.xml
new file mode 100644
index 0000000..2f8b3af
--- /dev/null
+++ b/atest/atest_unittests.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config to run atest unittests">
+    <option name="test-suite-tag" value="atest_unittests" />
+
+    <test class="com.android.tradefed.testtype.python.PythonBinaryHostTest" >
+        <option name="par-file-name" value="atest_unittests" />
+        <option name="test-timeout" value="2m" />
+    </test>
+</configuration>
diff --git a/atest/atest_utils.py b/atest/atest_utils.py
new file mode 100644
index 0000000..f784b86
--- /dev/null
+++ b/atest/atest_utils.py
@@ -0,0 +1,551 @@
+# Copyright 2017, 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.
+
+"""
+Utility functions for atest.
+"""
+
+from __future__ import print_function
+
+import hashlib
+import itertools
+import json
+import logging
+import os
+import pickle
+import re
+import subprocess
+import sys
+
+import atest_decorator
+import atest_error
+import constants
+
+from metrics import metrics_base
+from metrics import metrics_utils
+
+try:
+    # If PYTHON2
+    from urllib2 import urlopen
+except ImportError:
+    metrics_utils.handle_exc_and_send_exit_event(
+        constants.IMPORT_FAILURE)
+    from urllib.request import urlopen
+
+_BASH_RESET_CODE = '\033[0m\n'
+# Arbitrary number to limit stdout for failed runs in _run_limited_output.
+# Reason for its use is that the make command itself has its own carriage
+# return output mechanism that when collected line by line causes the streaming
+# full_output list to be extremely large.
+_FAILED_OUTPUT_LINE_LIMIT = 100
+# Regular expression to match the start of a ninja compile:
+# 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',
+                               'test_commands.json')
+BUILD_TOP_HASH = hashlib.md5(os.environ.get(constants.ANDROID_BUILD_TOP, '').
+                             encode()).hexdigest()
+TEST_INFO_CACHE_ROOT = os.path.join(os.path.expanduser('~'), '.atest',
+                                    'info_cache', BUILD_TOP_HASH[:8])
+_DEFAULT_TERMINAL_WIDTH = 80
+_BUILD_CMD = 'build/soong/soong_ui.bash'
+
+
+def get_build_cmd():
+    """Compose build command with relative path and flag "--make-mode".
+
+    Returns:
+        A list of soong build command.
+    """
+    make_cmd = ('%s/%s' %
+                (os.path.relpath(os.environ.get(
+                    constants.ANDROID_BUILD_TOP, os.getcwd()), os.getcwd()),
+                 _BUILD_CMD))
+    return [make_cmd, '--make-mode']
+
+
+def _capture_fail_section(full_log):
+    """Return the error message from the build output.
+
+    Args:
+        full_log: List of strings representing full output of build.
+
+    Returns:
+        capture_output: List of strings that are build errors.
+    """
+    am_capturing = False
+    capture_output = []
+    for line in full_log:
+        if am_capturing and _BUILD_COMPILE_STATUS.match(line):
+            break
+        if am_capturing or line.startswith(_BUILD_FAILURE):
+            capture_output.append(line)
+            am_capturing = True
+            continue
+    return capture_output
+
+
+def _run_limited_output(cmd, env_vars=None):
+    """Runs a given command and streams the output on a single line in stdout.
+
+    Args:
+        cmd: A list of strings representing the command to run.
+        env_vars: Optional arg. Dict of env vars to set during build.
+
+    Raises:
+        subprocess.CalledProcessError: When the command exits with a non-0
+            exitcode.
+    """
+    # Send stderr to stdout so we only have to deal with a single pipe.
+    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                            stderr=subprocess.STDOUT, env=env_vars)
+    sys.stdout.write('\n')
+    # Determine the width of the terminal. We'll need to clear this many
+    # characters when carriage returning. Set default value as 80.
+    term_width = _DEFAULT_TERMINAL_WIDTH
+    stty_size = os.popen('stty size').read()
+    if stty_size:
+        term_width = int(stty_size.split()[1])
+    white_space = " " * int(term_width)
+    full_output = []
+    while proc.poll() is None:
+        line = proc.stdout.readline()
+        # Readline will often return empty strings.
+        if not line:
+            continue
+        full_output.append(line.decode('utf-8'))
+        # Trim the line to the width of the terminal.
+        # Note: Does not handle terminal resizing, which is probably not worth
+        #       checking the width every loop.
+        if len(line) >= term_width:
+            line = line[:term_width - 1]
+        # Clear the last line we outputted.
+        sys.stdout.write('\r%s\r' % white_space)
+        sys.stdout.write('%s' % line.strip())
+        sys.stdout.flush()
+    # Reset stdout (on bash) to remove any custom formatting and newline.
+    sys.stdout.write(_BASH_RESET_CODE)
+    sys.stdout.flush()
+    # Wait for the Popen to finish completely before checking the returncode.
+    proc.wait()
+    if proc.returncode != 0:
+        # Parse out the build error to output.
+        output = _capture_fail_section(full_output)
+        if not output:
+            output = full_output
+        if len(output) >= _FAILED_OUTPUT_LINE_LIMIT:
+            output = output[-_FAILED_OUTPUT_LINE_LIMIT:]
+        output = 'Output (may be trimmed):\n%s' % ''.join(output)
+        raise subprocess.CalledProcessError(proc.returncode, cmd, output)
+
+
+def build(build_targets, verbose=False, env_vars=None):
+    """Shell out and make build_targets.
+
+    Args:
+        build_targets: A set of strings of build targets to make.
+        verbose: Optional arg. If True output is streamed to the console.
+                 If False, only the last line of the build output is outputted.
+        env_vars: Optional arg. Dict of env vars to set during build.
+
+    Returns:
+        Boolean of whether build command was successful, True if nothing to
+        build.
+    """
+    if not build_targets:
+        logging.debug('No build targets, skipping build.')
+        return True
+    full_env_vars = os.environ.copy()
+    if env_vars:
+        full_env_vars.update(env_vars)
+    print('\n%s\n%s' % (colorize("Building Dependencies...", constants.CYAN),
+                        ', '.join(build_targets)))
+    logging.debug('Building Dependencies: %s', ' '.join(build_targets))
+    cmd = get_build_cmd() + list(build_targets)
+    logging.debug('Executing command: %s', cmd)
+    try:
+        if verbose:
+            subprocess.check_call(cmd, stderr=subprocess.STDOUT,
+                                  env=full_env_vars)
+        else:
+            # TODO: Save output to a log file.
+            _run_limited_output(cmd, env_vars=full_env_vars)
+        logging.info('Build successful')
+        return True
+    except subprocess.CalledProcessError as err:
+        logging.error('Error building: %s', build_targets)
+        if err.output:
+            logging.error(err.output)
+        return False
+
+
+def _can_upload_to_result_server():
+    """Return True if we can talk to result server."""
+    # TODO: Also check if we have a slow connection to result server.
+    if constants.RESULT_SERVER:
+        try:
+            urlopen(constants.RESULT_SERVER,
+                    timeout=constants.RESULT_SERVER_TIMEOUT).close()
+            return True
+        # pylint: disable=broad-except
+        except Exception as err:
+            logging.debug('Talking to result server raised exception: %s', err)
+    return False
+
+
+def get_result_server_args():
+    """Return list of args for communication with result server."""
+    if _can_upload_to_result_server():
+        return constants.RESULT_SERVER_ARGS
+    return []
+
+
+def sort_and_group(iterable, key):
+    """Sort and group helper function."""
+    return itertools.groupby(sorted(iterable, key=key), key=key)
+
+
+def is_test_mapping(args):
+    """Check if the atest command intends to run tests in test mapping.
+
+    When atest runs tests in test mapping, it must have at most one test
+    specified. If a test is specified, it must be started with  `:`,
+    which means the test value is a test group name in TEST_MAPPING file, e.g.,
+    `:postsubmit`.
+
+    If any test mapping options is specified, the atest command must also be
+    set to run tests in test mapping files.
+
+    Args:
+        args: arg parsed object.
+
+    Returns:
+        True if the args indicates atest shall run tests in test mapping. False
+        otherwise.
+    """
+    return (
+        args.test_mapping or
+        args.include_subdirs or
+        not args.tests or
+        (len(args.tests) == 1 and args.tests[0][0] == ':'))
+
+@atest_decorator.static_var("cached_has_colors", {})
+def _has_colors(stream):
+    """Check the output stream is colorful.
+
+    Args:
+        stream: The standard file stream.
+
+    Returns:
+        True if the file stream can interpreter the ANSI color code.
+    """
+    cached_has_colors = _has_colors.cached_has_colors
+    if stream in cached_has_colors:
+        return cached_has_colors[stream]
+    else:
+        cached_has_colors[stream] = True
+    # Following from Python cookbook, #475186
+    if not hasattr(stream, "isatty"):
+        cached_has_colors[stream] = False
+        return False
+    if not stream.isatty():
+        # Auto color only on TTYs
+        cached_has_colors[stream] = False
+        return False
+    try:
+        import curses
+        curses.setupterm()
+        cached_has_colors[stream] = curses.tigetnum("colors") > 2
+    # pylint: disable=broad-except
+    except Exception as err:
+        logging.debug('Checking colorful raised exception: %s', err)
+        cached_has_colors[stream] = False
+    return cached_has_colors[stream]
+
+
+def colorize(text, color, highlight=False):
+    """ Convert to colorful string with ANSI escape code.
+
+    Args:
+        text: A string to print.
+        color: ANSI code shift for colorful print. They are defined
+               in constants_default.py.
+        highlight: True to print with highlight.
+
+    Returns:
+        Colorful string with ANSI escape code.
+    """
+    clr_pref = '\033[1;'
+    clr_suff = '\033[0m'
+    has_colors = _has_colors(sys.stdout)
+    if has_colors:
+        if highlight:
+            ansi_shift = 40 + color
+        else:
+            ansi_shift = 30 + color
+        clr_str = "%s%dm%s%s" % (clr_pref, ansi_shift, text, clr_suff)
+    else:
+        clr_str = text
+    return clr_str
+
+
+def colorful_print(text, color, highlight=False, auto_wrap=True):
+    """Print out the text with color.
+
+    Args:
+        text: A string to print.
+        color: ANSI code shift for colorful print. They are defined
+               in constants_default.py.
+        highlight: True to print with highlight.
+        auto_wrap: If True, Text wraps while print.
+    """
+    output = colorize(text, color, highlight)
+    if auto_wrap:
+        print(output)
+    else:
+        print(output, end="")
+
+
+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.
+    """
+    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 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 '
+              'Licenses (%s), Contributor License Agreement (%s), Privacy '
+              'Policy (%s) and Terms of Service (%s).'
+             ) % (anonymous,
+                  constants.CONTENT_LICENSES_URL,
+                  constants.CONTRIBUTOR_AGREEMENT_URL[user_type],
+                  constants.PRIVACY_POLICY_URL,
+                  constants.TERMS_SERVICE_URL
+                 )
+    print('\n==================')
+    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.
+    """
+    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 not _are_identical_cmds(test_cmds, former_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 _are_identical_cmds(current_cmds, former_cmds):
+    """Tell two commands are identical. Note that '--atest-log-file-path' is not
+    considered a critical argument, therefore, it will be removed during
+    the comparison. Also, atest can be ran in any place, so verifying relative
+    path is regardless as well.
+
+    Args:
+        current_cmds: A list of strings for running input tests.
+        former_cmds: A list of strings recorded from the previous run.
+
+    Returns:
+        True if both commands are identical, False otherwise.
+    """
+    def _normalize(cmd_list):
+        """Method that normalize commands.
+
+        Args:
+            cmd_list: A list with one element. E.g. ['cmd arg1 arg2 True']
+
+        Returns:
+            A list with elements. E.g. ['cmd', 'arg1', 'arg2', 'True']
+        """
+        _cmd = ''.join(cmd_list).encode('utf-8').split()
+        for cmd in _cmd:
+            if cmd.startswith('--atest-log-file-path'):
+                _cmd.remove(cmd)
+                continue
+            if _BUILD_CMD in cmd:
+                _cmd.remove(cmd)
+                _cmd.append(os.path.join('./', _BUILD_CMD))
+                continue
+        return _cmd
+
+    _current_cmds = _normalize(current_cmds)
+    _former_cmds = _normalize(former_cmds)
+    # Always sort cmd list to make it comparable.
+    _current_cmds.sort()
+    _former_cmds.sort()
+    return _current_cmds == _former_cmds
+
+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, protocol=2)
+    except (pickle.PicklingError, TypeError, IOError) as err:
+        # Won't break anything, just log this error, and collect the exception
+        # by metrics.
+        logging.debug('Exception raised: %s', err)
+        metrics_utils.handle_exc_and_send_exit_event(
+            constants.ACCESS_CACHE_FAILURE)
+
+
+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, ValueError, EOFError, IOError) as err:
+            # Won't break anything, just remove the old cache, log this error, and
+            # collect the exception by metrics.
+            logging.debug('Exception raised: %s', err)
+            os.remove(cache_file)
+            metrics_utils.handle_exc_and_send_exit_event(
+                constants.ACCESS_CACHE_FAILURE)
+    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)
+                metrics_utils.handle_exc_and_send_exit_event(
+                    constants.ACCESS_CACHE_FAILURE)
diff --git a/atest/atest_utils_unittest.py b/atest/atest_utils_unittest.py
new file mode 100755
index 0000000..85b2386
--- /dev/null
+++ b/atest/atest_utils_unittest.py
@@ -0,0 +1,386 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""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 constants
+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_FINDER_A = 'MODULE'
+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)
+TEST_INFO_A.test_finder = TEST_FINDER_A
+
+#pylint: disable=protected-access
+class AtestUtilsUnittests(unittest.TestCase):
+    """Unit tests for atest_utils.py"""
+
+    def test_capture_fail_section_has_fail_section(self):
+        """Test capture_fail_section when has fail section."""
+        test_list = ['AAAAAA', 'FAILED: Error1', '^\n', 'Error2\n',
+                     '[  6% 191/2997] BBBBBB\n', 'CCCCC',
+                     '[  20% 322/2997] DDDDDD\n', 'EEEEE']
+        want_list = ['FAILED: Error1', '^\n', 'Error2\n']
+        self.assertEqual(want_list,
+                         atest_utils._capture_fail_section(test_list))
+
+    def test_capture_fail_section_no_fail_section(self):
+        """Test capture_fail_section when no fail section."""
+        test_list = ['[ 6% 191/2997] XXXXX', 'YYYYY: ZZZZZ']
+        want_list = []
+        self.assertEqual(want_list,
+                         atest_utils._capture_fail_section(test_list))
+
+    def test_is_test_mapping(self):
+        """Test method is_test_mapping."""
+        tm_option_attributes = [
+            'test_mapping',
+            'include_subdirs'
+        ]
+        for attr_to_test in tm_option_attributes:
+            args = mock.Mock()
+            for attr in tm_option_attributes:
+                setattr(args, attr, attr == attr_to_test)
+            args.tests = []
+            self.assertTrue(
+                atest_utils.is_test_mapping(args),
+                'Failed to validate option %s' % attr_to_test)
+
+        args = mock.Mock()
+        for attr in tm_option_attributes:
+            setattr(args, attr, False)
+        args.tests = [':group_name']
+        self.assertTrue(atest_utils.is_test_mapping(args))
+
+        args = mock.Mock()
+        for attr in tm_option_attributes:
+            setattr(args, attr, False)
+        args.tests = [':test1', 'test2']
+        self.assertFalse(atest_utils.is_test_mapping(args))
+
+        args = mock.Mock()
+        for attr in tm_option_attributes:
+            setattr(args, attr, False)
+        args.tests = ['test2']
+        self.assertFalse(atest_utils.is_test_mapping(args))
+
+    @mock.patch('curses.tigetnum')
+    def test_has_colors(self, mock_curses_tigetnum):
+        """Test method _has_colors."""
+        # stream is file I/O
+        stream = open('/tmp/test_has_colors.txt', 'wb')
+        self.assertFalse(atest_utils._has_colors(stream))
+        stream.close()
+
+        # stream is not a tty(terminal).
+        stream = mock.Mock()
+        stream.isatty.return_value = False
+        self.assertFalse(atest_utils._has_colors(stream))
+
+        # stream is a tty(terminal) and colors < 2.
+        stream = mock.Mock()
+        stream.isatty.return_value = True
+        mock_curses_tigetnum.return_value = 1
+        self.assertFalse(atest_utils._has_colors(stream))
+
+        # stream is a tty(terminal) and colors > 2.
+        stream = mock.Mock()
+        stream.isatty.return_value = True
+        mock_curses_tigetnum.return_value = 256
+        self.assertTrue(atest_utils._has_colors(stream))
+
+
+    @mock.patch('atest_utils._has_colors')
+    def test_colorize(self, mock_has_colors):
+        """Test method colorize."""
+        original_str = "test string"
+        green_no = 2
+
+        # _has_colors() return False.
+        mock_has_colors.return_value = False
+        converted_str = atest_utils.colorize(original_str, green_no,
+                                             highlight=True)
+        self.assertEqual(original_str, converted_str)
+
+        # Green with highlight.
+        mock_has_colors.return_value = True
+        converted_str = atest_utils.colorize(original_str, green_no,
+                                             highlight=True)
+        green_highlight_string = '\x1b[1;42m%s\x1b[0m' % original_str
+        self.assertEqual(green_highlight_string, converted_str)
+
+        # Green, no highlight.
+        mock_has_colors.return_value = True
+        converted_str = atest_utils.colorize(original_str, green_no,
+                                             highlight=False)
+        green_no_highlight_string = '\x1b[1;32m%s\x1b[0m' % original_str
+        self.assertEqual(green_no_highlight_string, converted_str)
+
+
+    @mock.patch('atest_utils._has_colors')
+    def test_colorful_print(self, mock_has_colors):
+        """Test method colorful_print."""
+        testing_str = "color_print_test"
+        green_no = 2
+
+        # _has_colors() return False.
+        mock_has_colors.return_value = False
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        atest_utils.colorful_print(testing_str, green_no, highlight=True,
+                                   auto_wrap=False)
+        sys.stdout = sys.__stdout__
+        uncolored_string = testing_str
+        self.assertEqual(capture_output.getvalue(), uncolored_string)
+
+        # Green with highlight, but no wrap.
+        mock_has_colors.return_value = True
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        atest_utils.colorful_print(testing_str, green_no, highlight=True,
+                                   auto_wrap=False)
+        sys.stdout = sys.__stdout__
+        green_highlight_no_wrap_string = '\x1b[1;42m%s\x1b[0m' % testing_str
+        self.assertEqual(capture_output.getvalue(),
+                         green_highlight_no_wrap_string)
+
+        # Green, no highlight, no wrap.
+        mock_has_colors.return_value = True
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        atest_utils.colorful_print(testing_str, green_no, highlight=False,
+                                   auto_wrap=False)
+        sys.stdout = sys.__stdout__
+        green_no_high_no_wrap_string = '\x1b[1;32m%s\x1b[0m' % testing_str
+        self.assertEqual(capture_output.getvalue(),
+                         green_no_high_no_wrap_string)
+
+        # Green with highlight and wrap.
+        mock_has_colors.return_value = True
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        atest_utils.colorful_print(testing_str, green_no, highlight=True,
+                                   auto_wrap=True)
+        sys.stdout = sys.__stdout__
+        green_highlight_wrap_string = '\x1b[1;42m%s\x1b[0m\n' % testing_str
+        self.assertEqual(capture_output.getvalue(), green_highlight_wrap_string)
+
+        # Green with wrap, but no highlight.
+        mock_has_colors.return_value = True
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        atest_utils.colorful_print(testing_str, green_no, highlight=False,
+                                   auto_wrap=True)
+        sys.stdout = sys.__stdout__
+        green_wrap_no_highlight_string = '\x1b[1;32m%s\x1b[0m\n' % testing_str
+        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, 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 = '[email protected]'
+        mock_hostname.return_value = 'abc.com'
+        self.assertTrue(atest_utils.is_external_run())
+
+        mock_output.return_value = '[email protected]'
+        mock_hostname.return_value = 'abc.google.com'
+        self.assertFalse(atest_utils.is_external_run())
+
+        mock_output.return_value = '[email protected]'
+        mock_hostname.return_value = 'abc.google.def.com'
+        self.assertTrue(atest_utils.is_external_run())
+
+        mock_output.return_value = '[email protected]'
+        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('metrics.metrics_base.get_user_type')
+    def test_print_data_collection_notice(self, mock_get_user_type):
+        """Test method print_data_collection_notice."""
+
+        # 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'
+                      ' Content Licenses (https://source.android.com/setup/start/licenses),'
+                      ' Contributor License Agreement (https://opensource.google.com/docs/cla/),'
+                      ' Privacy Policy (https://policies.google.com/privacy) and'
+                      ' Terms of Service (https://policies.google.com/terms).'
+                      '\n==================\n\n')
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        atest_utils.print_data_collection_notice()
+        sys.stdout = sys.__stdout__
+        uncolored_string = notice_str
+        self.assertEqual(capture_output.getvalue(), uncolored_string)
+
+        # 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'
+                      ' Content Licenses (https://source.android.com/setup/start/licenses),'
+                      ' Contributor License Agreement (https://cla.developers.google.com/),'
+                      ' Privacy Policy (https://policies.google.com/privacy) and'
+                      ' Terms of Service (https://policies.google.com/terms).'
+                      '\n==================\n\n')
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        atest_utils.print_data_collection_notice()
+        sys.stdout = sys.__stdout__
+        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_testinfo_sets(
+            self, set([TEST_INFO_A]),
+            atest_utils.load_test_info_cache(test_reference, test_cache_dir))
+
+    @mock.patch('os.getcwd')
+    def test_get_build_cmd(self, mock_cwd):
+        """Test method get_build_cmd."""
+        build_top = '/home/a/b/c'
+        rel_path = 'd/e'
+        mock_cwd.return_value = os.path.join(build_top, rel_path)
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: build_top}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            expected_cmd = ['../../build/soong/soong_ui.bash', '--make-mode']
+            self.assertEqual(expected_cmd, atest_utils.get_build_cmd())
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/atest/bug_detector.py b/atest/bug_detector.py
new file mode 100644
index 0000000..25438d2
--- /dev/null
+++ b/atest/bug_detector.py
@@ -0,0 +1,140 @@
+# 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 logging
+import json
+import os
+
+import constants
+
+from metrics import metrics_utils
+
+_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.
+        """
+        history = {}
+        if os.path.exists(self.file):
+            with open(self.file) as json_file:
+                try:
+                    history = json.load(json_file)
+                except ValueError as e:
+                    logging.debug(e)
+                    metrics_utils.handle_exc_and_send_exit_event(
+                        constants.ACCESS_HISTORY_FAILURE)
+        return history
+
+    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:
+            try:
+                json.dump(self.history, outfile, indent=0)
+            except ValueError as e:
+                logging.debug(e)
+                metrics_utils.handle_exc_and_send_exit_event(
+                    constants.ACCESS_HISTORY_FAILURE)
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
new file mode 100644
index 0000000..97d7616
--- /dev/null
+++ b/atest/cli_translator.py
@@ -0,0 +1,504 @@
+# Copyright 2017, 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.
+
+#pylint: disable=too-many-lines
+"""
+Command Line Translator for atest.
+"""
+
+from __future__ import print_function
+
+import fnmatch
+import json
+import logging
+import os
+import re
+import sys
+import time
+
+import atest_error
+import atest_utils
+import constants
+import test_finder_handler
+import test_mapping
+
+from metrics import metrics
+from metrics import metrics_utils
+from test_finders import module_finder
+
+TEST_MAPPING = 'TEST_MAPPING'
+FUZZY_FINDER = 'FUZZY'
+CACHE_FINDER = 'CACHE'
+
+# 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):
+    """
+    CLITranslator class contains public method translate() and some private
+    helper methods. The atest tool can call the translate() method with a list
+    of strings, each string referencing a test to run. Translate() will
+    "translate" this list of test strings into a list of build targets and a
+    list of TradeFederation run commands.
+
+    Translation steps for a test string reference:
+        1. Narrow down the type of reference the test string could be, i.e.
+           whether it could be referencing a Module, Class, Package, etc.
+        2. Try to find the test files assuming the test string is one of these
+           types of reference.
+        3. If test files found, generate Build Targets and the Run Command.
+    """
+
+    def __init__(self, module_info=None):
+        """CLITranslator constructor
+
+        Args:
+            module_info: ModuleInfo class that has cached module-info.json.
+        """
+        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.
+
+        Args:
+            test: A string representing test references.
+            tm_test_detail: The TestDetail of test configured in TEST_MAPPING
+                files.
+
+        Returns:
+            Set of TestInfos based on the given test.
+        """
+        test_infos = set()
+        test_find_starts = time.time()
+        test_found = False
+        test_finders = []
+        test_info_str = ''
+        find_test_err_msg = None
+        for finder in test_finder_handler.get_find_methods_for_test(
+                self.mod_info, test):
+            # For tests in TEST_MAPPING, find method is only related to
+            # test name, so the details can be set after test_info object
+            # is created.
+            try:
+                found_test_infos = finder.find_method(
+                    finder.test_finder_instance, test)
+            except atest_error.TestDiscoveryException as e:
+                find_test_err_msg = e
+            if found_test_infos:
+                finder_info = finder.finder_info
+                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
+                    if finder_info != CACHE_FINDER:
+                        test_info.test_finder = finder_info
+                    test_infos.add(test_info)
+                test_found = True
+                print("Found '%s' as %s" % (
+                    atest_utils.colorize(test, constants.GREEN),
+                    finder_info))
+                if finder_info == CACHE_FINDER and test_infos:
+                    test_finders.append(list(test_infos)[0].test_finder)
+                test_finders.append(finder_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.update(f_results)
+                test_found = True
+                test_finders.append(FUZZY_FINDER)
+        metrics.FindTestFinishEvent(
+            duration=metrics_utils.convert_duration(
+                time.time() - test_find_starts),
+            success=test_found,
+            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):
+        """ Fuzzy search and print message.
+
+        Args:
+            test: A string representing test references
+            find_test_err_msg: A string of find test error message.
+
+        Returns:
+            A list of TestInfos if found, otherwise None.
+        """
+        print('No test found for: %s' %
+              atest_utils.colorize(test, constants.RED))
+        # Currently we focus on guessing module names. Append names on
+        # results if more finders support fuzzy searching.
+        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):
+            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:
+            print('No matching result for {0}.'.format(test))
+        if find_test_err_msg:
+            print('%s\n' % (atest_utils.colorize(
+                find_test_err_msg, constants.MAGENTA)))
+        else:
+            print('(This can happen after a repo sync or if the test'
+                  ' is new. Running: with "%s" may resolve the issue.)'
+                  '\n' % (atest_utils.colorize(
+                      constants.REBUILD_MODULE_INFO_FLAG,
+                      constants.RED)))
+        return None
+
+    def _get_test_infos(self, tests, test_mapping_test_details=None):
+        """Return set of TestInfos based on passed in tests.
+
+        Args:
+            tests: List of strings representing test references.
+            test_mapping_test_details: List of TestDetail for tests configured
+                in TEST_MAPPING files.
+
+        Returns:
+            Set of TestInfos based on the passed in tests.
+        """
+        test_infos = set()
+        if not test_mapping_test_details:
+            test_mapping_test_details = [None] * len(tests)
+        for test, tm_test_detail in zip(tests, test_mapping_test_details):
+            found_test_infos = self._find_test_infos(test, tm_test_detail)
+            test_infos.update(found_test_infos)
+        return test_infos
+
+    def _confirm_running(self, results):
+        """Listen to an answer from raw input.
+
+        Args:
+            results: A list of results.
+
+        Returns:
+            True is the answer is affirmative.
+        """
+        decision = raw_input('Did you mean {0}? [Y/n] '.format(
+            atest_utils.colorize(results[0], constants.GREEN)))
+        return decision in constants.AFFIRMATIVES
+
+    def _print_fuzzy_searching_results(self, results):
+        """Print modules when fuzzy searching gives multiple results.
+
+        If the result is lengthy, just print the first 10 items only since we
+        have already given enough-accurate result.
+
+        Args:
+            results: A list of guessed testable module names.
+
+        """
+        atest_utils.colorful_print('Did you mean the following modules?',
+                                   constants.WHITE)
+        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.
+
+        Args:
+            test_mapping_file: Path to a TEST_MAPPING file.
+
+        Returns:
+            A tuple of (all_tests, imports), where
+            all_tests is a dictionary of all tests in the TEST_MAPPING file,
+                grouped by test group.
+            imports is a list of test_mapping.Import to include other test
+                mapping files.
+        """
+        all_tests = {}
+        imports = []
+        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:
+                    imports.append(
+                        test_mapping.Import(test_mapping_file, import_detail))
+            else:
+                grouped_tests = all_tests.setdefault(test_group_name, set())
+                tests = []
+                for test in test_list:
+                    test_mod_info = self.mod_info.name_to_module_info.get(
+                        test['name'])
+                    if not test_mod_info:
+                        print('WARNING: %s is not a valid build target and '
+                              'may not be discoverable by TreeHugger. If you '
+                              'want to specify a class or test-package, '
+                              'please set \'name\' to the test module and use '
+                              '\'options\' to specify the right tests via '
+                              '\'include-filter\'.\nNote: this can also occur '
+                              'if the test module is not built for your '
+                              'current lunch target.\n' %
+                              atest_utils.colorize(test['name'], constants.RED))
+                    elif not any(x in test_mod_info['compatibility_suites'] for
+                                 x in constants.TEST_MAPPING_SUITES):
+                        print('WARNING: Please add %s to either suite: %s for '
+                              'this TEST_MAPPING file to work with TreeHugger.' %
+                              (atest_utils.colorize(test['name'],
+                                                    constants.RED),
+                               atest_utils.colorize(constants.TEST_MAPPING_SUITES,
+                                                    constants.GREEN)))
+                    tests.append(test_mapping.TestDetail(test))
+                grouped_tests.update(tests)
+        return all_tests, imports
+
+    def _find_files(self, path, file_name=TEST_MAPPING):
+        """Find all files with given name under the given path.
+
+        Args:
+            path: A string of path in source.
+
+        Returns:
+            A list of paths of the files with the matching name under the given
+            path.
+        """
+        test_mapping_files = []
+        for root, _, filenames in os.walk(path):
+            for filename in fnmatch.filter(filenames, file_name):
+                test_mapping_files.append(os.path.join(root, filename))
+        return test_mapping_files
+
+    def _get_tests_from_test_mapping_files(
+            self, test_group, test_mapping_files):
+        """Get tests in the given test mapping files with the match group.
+
+        Args:
+            test_group: Group of tests to run. Default is set to `presubmit`.
+            test_mapping_files: A list of path of TEST_MAPPING files.
+
+        Returns:
+            A tuple of (tests, all_tests, imports), where,
+            tests is a set of tests (test_mapping.TestDetail) defined in
+            TEST_MAPPING file of the given path, and its parent directories,
+            with matching test_group.
+            all_tests is a dictionary of all tests in TEST_MAPPING files,
+            grouped by test group.
+            imports is a list of test_mapping.Import objects that contains the
+            details of where to import a TEST_MAPPING file.
+        """
+        all_imports = []
+        # Read and merge the tests in all TEST_MAPPING files.
+        merged_all_tests = {}
+        for test_mapping_file in test_mapping_files:
+            all_tests, imports = self._read_tests_in_test_mapping(
+                test_mapping_file)
+            all_imports.extend(imports)
+            for test_group_name, test_list in all_tests.items():
+                grouped_tests = merged_all_tests.setdefault(
+                    test_group_name, set())
+                grouped_tests.update(test_list)
+
+        tests = set(merged_all_tests.get(test_group, []))
+        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
+
+    # pylint: disable=too-many-arguments
+    # pylint: disable=too-many-locals
+    def _find_tests_by_test_mapping(
+            self, path='', test_group=constants.TEST_GROUP_PRESUBMIT,
+            file_name=TEST_MAPPING, include_subdirs=False, checked_files=None):
+        """Find tests defined in TEST_MAPPING in the given path.
+
+        Args:
+            path: A string of path in source. Default is set to '', i.e., CWD.
+            test_group: Group of tests to run. Default is set to `presubmit`.
+            file_name: Name of TEST_MAPPING file. Default is set to
+                `TEST_MAPPING`. The argument is added for testing purpose.
+            include_subdirs: True to include tests in TEST_MAPPING files in sub
+                directories.
+            checked_files: Paths of TEST_MAPPING files that have been checked.
+
+        Returns:
+            A tuple of (tests, all_tests), where,
+            tests is a set of tests (test_mapping.TestDetail) defined in
+            TEST_MAPPING file of the given path, and its parent directories,
+            with matching test_group.
+            all_tests is a dictionary of all tests in TEST_MAPPING files,
+            grouped by test group.
+        """
+        path = os.path.realpath(path)
+        test_mapping_files = set()
+        all_tests = {}
+        test_mapping_file = os.path.join(path, file_name)
+        if os.path.exists(test_mapping_file):
+            test_mapping_files.add(test_mapping_file)
+        # Include all TEST_MAPPING files in sub-directories if `include_subdirs`
+        # is set to True.
+        if include_subdirs:
+            test_mapping_files.update(self._find_files(path, file_name))
+        # Include all possible TEST_MAPPING files in parent directories.
+        root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, os.sep)
+        while path != root_dir and path != os.sep:
+            path = os.path.dirname(path)
+            test_mapping_file = os.path.join(path, file_name)
+            if os.path.exists(test_mapping_file):
+                test_mapping_files.add(test_mapping_file)
+
+        if checked_files is None:
+            checked_files = set()
+        test_mapping_files.difference_update(checked_files)
+        checked_files.update(test_mapping_files)
+        if not test_mapping_files:
+            return test_mapping_files, all_tests
+
+        tests, all_tests, imports = self._get_tests_from_test_mapping_files(
+            test_group, test_mapping_files)
+
+        # Load TEST_MAPPING files from imports recursively.
+        if imports:
+            for import_detail in imports:
+                path = import_detail.get_path()
+                # (b/110166535 #19) Import path might not exist if a project is
+                # located in different directory in different branches.
+                if path is None:
+                    logging.warn(
+                        'Failed to import TEST_MAPPING at %s', import_detail)
+                    continue
+                # Search for tests based on the imported search path.
+                import_tests, import_all_tests = (
+                    self._find_tests_by_test_mapping(
+                        path, test_group, file_name, include_subdirs,
+                        checked_files))
+                # Merge the collections
+                tests.update(import_tests)
+                for group, grouped_tests in import_all_tests.items():
+                    all_tests.setdefault(group, set()).update(grouped_tests)
+
+        return tests, all_tests
+
+    def _gather_build_targets(self, test_infos):
+        targets = set()
+        for test_info in test_infos:
+            targets |= test_info.build_targets
+        return targets
+
+    def _get_test_mapping_tests(self, args):
+        """Find the tests in TEST_MAPPING files.
+
+        Args:
+            args: arg parsed object.
+
+        Returns:
+            A tuple of (test_names, test_details_list), where
+            test_names: a list of test name
+            test_details_list: a list of test_mapping.TestDetail objects for
+                the tests in TEST_MAPPING files with matching test group.
+        """
+        # Pull out tests from test mapping
+        src_path = ''
+        test_group = constants.TEST_GROUP_PRESUBMIT
+        if args.tests:
+            if ':' in args.tests[0]:
+                src_path, test_group = args.tests[0].split(':')
+            else:
+                src_path = args.tests[0]
+
+        test_details, all_test_details = self._find_tests_by_test_mapping(
+            path=src_path, test_group=test_group,
+            include_subdirs=args.include_subdirs, checked_files=set())
+        test_details_list = list(test_details)
+        if not test_details_list:
+            logging.warn(
+                'No tests of group `%s` found in TEST_MAPPING at %s or its '
+                'parent directories.\nYou might be missing atest arguments,'
+                ' try `atest --help` for more information',
+                test_group, os.path.realpath(''))
+            if all_test_details:
+                tests = ''
+                for test_group, test_list in all_test_details.items():
+                    tests += '%s:\n' % test_group
+                    for test_detail in sorted(test_list):
+                        tests += '\t%s\n' % test_detail
+                logging.warn(
+                    'All available tests in TEST_MAPPING files are:\n%s',
+                    tests)
+            metrics_utils.send_exit_event(constants.EXIT_CODE_TEST_NOT_FOUND)
+            sys.exit(constants.EXIT_CODE_TEST_NOT_FOUND)
+
+        logging.debug(
+            'Test details:\n%s',
+            '\n'.join([str(detail) for detail in test_details_list]))
+        test_names = [detail.name for detail in test_details_list]
+        return test_names, test_details_list
+
+
+    def translate(self, args):
+        """Translate atest command line into build targets and run commands.
+
+        Args:
+            args: arg parsed object.
+
+        Returns:
+            A tuple with set of build_target strings and list of TestInfos.
+        """
+        tests = args.tests
+        # Test details from TEST_MAPPING files
+        test_details_list = None
+        if atest_utils.is_test_mapping(args):
+            tests, test_details_list = self._get_test_mapping_tests(args)
+        atest_utils.colorful_print("\nFinding Tests...", constants.CYAN)
+        logging.debug('Finding Tests: %s', tests)
+        start = time.time()
+        test_infos = self._get_test_infos(tests, test_details_list)
+        logging.debug('Found tests in %ss', time.time() - start)
+        for test_info in test_infos:
+            logging.debug('%s\n', test_info)
+        build_targets = self._gather_build_targets(test_infos)
+        return build_targets, test_infos
diff --git a/atest/cli_translator_unittest.py b/atest/cli_translator_unittest.py
new file mode 100755
index 0000000..1b6137b
--- /dev/null
+++ b/atest/cli_translator_unittest.py
@@ -0,0 +1,374 @@
+#!/usr/bin/env python
+#
+# Copyright 2017, 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 cli_translator."""
+
+import unittest
+import json
+import os
+import re
+import sys
+import mock
+
+import cli_translator as cli_t
+import constants
+import test_finder_handler
+import test_mapping
+import unittest_constants as uc
+import unittest_utils
+from metrics import metrics
+from test_finders import module_finder
+from test_finders import test_finder_base
+
+# Import StringIO in Python2/3 compatible way.
+if sys.version_info[0] == 2:
+    from StringIO import StringIO
+else:
+    from io import StringIO
+
+# TEST_MAPPING related consts
+TEST_MAPPING_TOP_DIR = os.path.join(uc.TEST_DATA_DIR, 'test_mapping')
+TEST_MAPPING_DIR = os.path.join(TEST_MAPPING_TOP_DIR, 'folder1')
+TEST_1 = test_mapping.TestDetail({'name': 'test1', 'host': True})
+TEST_2 = test_mapping.TestDetail({'name': 'test2'})
+TEST_3 = test_mapping.TestDetail({'name': 'test3'})
+TEST_4 = test_mapping.TestDetail({'name': 'test4'})
+TEST_5 = test_mapping.TestDetail({'name': 'test5'})
+TEST_6 = test_mapping.TestDetail({'name': 'test6'})
+TEST_7 = test_mapping.TestDetail({'name': 'test7'})
+TEST_8 = test_mapping.TestDetail({'name': 'test8'})
+TEST_9 = test_mapping.TestDetail({'name': 'test9'})
+TEST_10 = test_mapping.TestDetail({'name': 'test10'})
+
+SEARCH_DIR_RE = re.compile(r'^find ([^ ]*).*$')
+
+
+#pylint: disable=unused-argument
+def gettestinfos_side_effect(test_names, test_mapping_test_details=None):
+    """Mock return values for _get_test_info."""
+    test_infos = set()
+    for test_name in test_names:
+        if test_name == uc.MODULE_NAME:
+            test_infos.add(uc.MODULE_INFO)
+        if test_name == uc.CLASS_NAME:
+            test_infos.add(uc.CLASS_INFO)
+    return test_infos
+
+
+#pylint: disable=protected-access
+#pylint: disable=no-self-use
+class CLITranslatorUnittests(unittest.TestCase):
+    """Unit tests for cli_t.py"""
+
+    def setUp(self):
+        """Run before execution of every test"""
+        self.ctr = cli_t.CLITranslator()
+
+        # Create a mock of args.
+        self.args = mock.Mock
+        self.args.tests = []
+        # 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 = {}
+
+    def tearDown(self):
+        """Run after execution of every test"""
+        reload(uc)
+
+    @mock.patch('__builtin__.raw_input', return_value='n')
+    @mock.patch.object(module_finder.ModuleFinder, 'find_test_by_module_name')
+    @mock.patch.object(module_finder.ModuleFinder, 'get_fuzzy_searching_results')
+    @mock.patch.object(metrics, 'FindTestFinishEvent')
+    @mock.patch.object(test_finder_handler, 'get_find_methods_for_test')
+    # pylint: disable=too-many-locals
+    def test_get_test_infos(self, mock_getfindmethods, _metrics, mock_getfuzzyresults,
+                            mock_findtestbymodule, mock_raw_input):
+        """Test _get_test_infos method."""
+        ctr = cli_t.CLITranslator()
+        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_INFOS
+                                                if test == uc.MODULE_NAME
+                                                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]
+
+        # Let's make sure we return what we expect.
+        expected_test_infos = {uc.MODULE_INFO}
+        mock_getfindmethods.return_value = [
+            test_finder_base.Finder(None, find_method_return_module_info, 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}
+        mock_getfindmethods.return_value = [
+            test_finder_base.Finder(None, find_method_return_module_class_info,
+                                    None)]
+        unittest_utils.assert_strict_equal(
+            self, ctr._get_test_infos(mult_test), expected_test_infos)
+
+        # Check return null set when we have no tests found or multiple results.
+        mock_getfindmethods.return_value = [
+            test_finder_base.Finder(None, find_method_return_nothing, None)]
+        null_test_info = set()
+        mock_getfuzzyresults.return_value = []
+        self.assertEqual(null_test_info, ctr._get_test_infos(one_test))
+        self.assertEqual(null_test_info, ctr._get_test_infos(mult_test))
+
+        # Check returning test_info when the user says Yes.
+        mock_raw_input.return_value = "Y"
+        mock_getfindmethods.return_value = [
+            test_finder_base.Finder(None, find_method_return_module_info, None)]
+        mock_getfuzzyresults.return_value = one_test
+        mock_findtestbymodule.return_value = uc.MODULE_INFO
+        unittest_utils.assert_strict_equal(
+            self, ctr._get_test_infos([uc.TYPO_MODULE_NAME]), {uc.MODULE_INFO})
+
+        # 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}
+        mock_getfindmethods.return_value = [
+            test_finder_base.Finder(None, find_method_return_module_class_info,
+                                    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 == uc.MODULE_INFO:
+                self.assertEqual(
+                    test_detail1.options,
+                    test_info.data[constants.TI_MODULE_ARG])
+            else:
+                self.assertEqual(
+                    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):
+        """Test translate method for tests by class name."""
+        # Check that we can find a class.
+        self.args.tests = [uc.CLASS_NAME]
+        targets, test_infos = self.ctr.translate(self.args)
+        unittest_utils.assert_strict_equal(
+            self, targets, uc.CLASS_BUILD_TARGETS)
+        unittest_utils.assert_strict_equal(self, test_infos, {uc.CLASS_INFO})
+
+    @mock.patch.object(cli_t.CLITranslator, '_get_test_infos',
+                       side_effect=gettestinfos_side_effect)
+    def test_translate_module(self, _info):
+        """Test translate method for tests by module or class name."""
+        # Check that we get all the build targets we expect.
+        self.args.tests = [uc.MODULE_NAME, uc.CLASS_NAME]
+        targets, test_infos = self.ctr.translate(self.args)
+        unittest_utils.assert_strict_equal(
+            self, targets, uc.MODULE_CLASS_COMBINED_BUILD_TARGETS)
+        unittest_utils.assert_strict_equal(self, test_infos, {uc.MODULE_INFO,
+                                                              uc.CLASS_INFO})
+
+    @mock.patch.object(cli_t.CLITranslator, '_find_tests_by_test_mapping')
+    @mock.patch.object(cli_t.CLITranslator, '_get_test_infos',
+                       side_effect=gettestinfos_side_effect)
+    def test_translate_test_mapping(self, _info, mock_testmapping):
+        """Test translate method for tests in test mapping."""
+        # Check that test mappings feeds into get_test_info properly.
+        test_detail1 = test_mapping.TestDetail(uc.TEST_MAPPING_TEST)
+        test_detail2 = test_mapping.TestDetail(uc.TEST_MAPPING_TEST_WITH_OPTION)
+        mock_testmapping.return_value = ([test_detail1, test_detail2], None)
+        self.args.tests = []
+        targets, test_infos = self.ctr.translate(self.args)
+        unittest_utils.assert_strict_equal(
+            self, targets, uc.MODULE_CLASS_COMBINED_BUILD_TARGETS)
+        unittest_utils.assert_strict_equal(self, test_infos, {uc.MODULE_INFO,
+                                                              uc.CLASS_INFO})
+
+    @mock.patch.object(cli_t.CLITranslator, '_find_tests_by_test_mapping')
+    @mock.patch.object(cli_t.CLITranslator, '_get_test_infos',
+                       side_effect=gettestinfos_side_effect)
+    def test_translate_test_mapping_all(self, _info, mock_testmapping):
+        """Test translate method for tests in test mapping."""
+        # Check that test mappings feeds into get_test_info properly.
+        test_detail1 = test_mapping.TestDetail(uc.TEST_MAPPING_TEST)
+        test_detail2 = test_mapping.TestDetail(uc.TEST_MAPPING_TEST_WITH_OPTION)
+        mock_testmapping.return_value = ([test_detail1, test_detail2], None)
+        self.args.tests = ['src_path:all']
+        self.args.test_mapping = True
+        targets, test_infos = self.ctr.translate(self.args)
+        unittest_utils.assert_strict_equal(
+            self, targets, uc.MODULE_CLASS_COMBINED_BUILD_TARGETS)
+        unittest_utils.assert_strict_equal(self, test_infos, {uc.MODULE_INFO,
+                                                              uc.CLASS_INFO})
+
+    def test_find_tests_by_test_mapping_presubmit(self):
+        """Test _find_tests_by_test_mapping method to locate presubmit tests."""
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: uc.TEST_DATA_DIR}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            tests, all_tests = self.ctr._find_tests_by_test_mapping(
+                path=TEST_MAPPING_DIR, file_name='test_mapping_sample',
+                checked_files=set())
+        expected = set([TEST_1, TEST_2, TEST_5, TEST_7, TEST_9])
+        expected_all_tests = {'presubmit': expected,
+                              'postsubmit': set(
+                                  [TEST_3, TEST_6, TEST_8, TEST_10]),
+                              'other_group': set([TEST_4])}
+        self.assertEqual(expected, tests)
+        self.assertEqual(expected_all_tests, all_tests)
+
+    def test_find_tests_by_test_mapping_postsubmit(self):
+        """Test _find_tests_by_test_mapping method to locate postsubmit tests.
+        """
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: uc.TEST_DATA_DIR}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            tests, all_tests = self.ctr._find_tests_by_test_mapping(
+                path=TEST_MAPPING_DIR,
+                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_3, TEST_6, TEST_8, TEST_10])
+        expected_all_tests = {'presubmit': expected_presubmit,
+                              'postsubmit': set(
+                                  [TEST_3, TEST_6, TEST_8, TEST_10]),
+                              'other_group': set([TEST_4])}
+        self.assertEqual(expected, tests)
+        self.assertEqual(expected_all_tests, all_tests)
+
+    def test_find_tests_by_test_mapping_all_group(self):
+        """Test _find_tests_by_test_mapping method to locate postsubmit tests.
+        """
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: uc.TEST_DATA_DIR}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            tests, all_tests = self.ctr._find_tests_by_test_mapping(
+                path=TEST_MAPPING_DIR, test_group=constants.TEST_GROUP_ALL,
+                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_4, TEST_5, TEST_6, TEST_7, TEST_8,
+            TEST_9, TEST_10])
+        expected_all_tests = {'presubmit': expected_presubmit,
+                              'postsubmit': set(
+                                  [TEST_3, TEST_6, TEST_8, TEST_10]),
+                              'other_group': set([TEST_4])}
+        self.assertEqual(expected, tests)
+        self.assertEqual(expected_all_tests, all_tests)
+
+    def test_find_tests_by_test_mapping_include_subdir(self):
+        """Test _find_tests_by_test_mapping method to include sub directory."""
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: uc.TEST_DATA_DIR}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            tests, all_tests = self.ctr._find_tests_by_test_mapping(
+                path=TEST_MAPPING_TOP_DIR, file_name='test_mapping_sample',
+                include_subdirs=True, checked_files=set())
+        expected = set([TEST_1, TEST_2, TEST_5, TEST_7, TEST_9])
+        expected_all_tests = {'presubmit': expected,
+                              'postsubmit': set([
+                                  TEST_3, TEST_6, TEST_8, TEST_10]),
+                              'other_group': set([TEST_4])}
+        self.assertEqual(expected, tests)
+        self.assertEqual(expected_all_tests, all_tests)
+
+    @mock.patch('__builtin__.raw_input', return_value='')
+    def test_confirm_running(self, mock_raw_input):
+        """Test _confirm_running method."""
+        self.assertTrue(self.ctr._confirm_running([TEST_1]))
+        mock_raw_input.return_value = 'N'
+        self.assertFalse(self.ctr._confirm_running([TEST_2]))
+
+    def test_print_fuzzy_searching_results(self):
+        """Test _print_fuzzy_searching_results"""
+        modules = [uc.MODULE_NAME, uc.MODULE2_NAME]
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        self.ctr._print_fuzzy_searching_results(modules)
+        sys.stdout = sys.__stdout__
+        output = 'Did you mean the following modules?\n{0}\n{1}\n'.format(
+            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.py b/atest/constants.py
new file mode 100644
index 0000000..fad8ef5
--- /dev/null
+++ b/atest/constants.py
@@ -0,0 +1,29 @@
+# Copyright 2017, 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.
+
+"""
+Imports the various constant files that are available (default, google, etc).
+"""
+#pylint: disable=wildcard-import
+#pylint: disable=unused-wildcard-import
+
+from constants_default import *
+
+
+# Now try to import the various constant files outside this repo to overwrite
+# the globals as desired.
+try:
+    from constants_google import *
+except ImportError:
+    pass
diff --git a/atest/constants_default.py b/atest/constants_default.py
new file mode 100644
index 0000000..78c043a
--- /dev/null
+++ b/atest/constants_default.py
@@ -0,0 +1,222 @@
+# Copyright 2017, 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.
+
+"""
+Various globals used by atest.
+"""
+
+import os
+import re
+
+MODE = 'DEFAULT'
+
+# Result server constants for atest_utils.
+RESULT_SERVER = ''
+RESULT_SERVER_ARGS = []
+RESULT_SERVER_TIMEOUT = 5
+# Result arguments if tests are configured in TEST_MAPPING.
+TEST_MAPPING_RESULT_SERVER_ARGS = []
+
+# Google service key for gts tests.
+GTS_GOOGLE_SERVICE_ACCOUNT = ''
+
+# Arg constants.
+WAIT_FOR_DEBUGGER = 'WAIT_FOR_DEBUGGER'
+DISABLE_INSTALL = 'DISABLE_INSTALL'
+DISABLE_TEARDOWN = 'DISABLE_TEARDOWN'
+PRE_PATCH_ITERATIONS = 'PRE_PATCH_ITERATIONS'
+POST_PATCH_ITERATIONS = 'POST_PATCH_ITERATIONS'
+PRE_PATCH_FOLDER = 'PRE_PATCH_FOLDER'
+POST_PATCH_FOLDER = 'POST_PATCH_FOLDER'
+SERIAL = 'SERIAL'
+ALL_ABI = 'ALL_ABI'
+HOST = 'HOST'
+CUSTOM_ARGS = 'CUSTOM_ARGS'
+DRY_RUN = 'DRY_RUN'
+ANDROID_SERIAL = 'ANDROID_SERIAL'
+INSTANT = 'INSTANT'
+USER_TYPE = 'USER_TYPE'
+ITERATIONS = 'ITERATIONS'
+RERUN_UNTIL_FAILURE = 'RERUN_UNTIL_FAILURE'
+RETRY_ANY_FAILURE = 'RETRY_ANY_FAILURE'
+
+# Application exit codes.
+EXIT_CODE_SUCCESS = 0
+EXIT_CODE_ENV_NOT_SETUP = 1
+EXIT_CODE_BUILD_FAILURE = 2
+EXIT_CODE_ERROR = 3
+EXIT_CODE_TEST_NOT_FOUND = 4
+EXIT_CODE_TEST_FAILURE = 5
+EXIT_CODE_VERIFY_FAILURE = 6
+
+# Codes of specific events. These are exceptions that don't stop anything
+# but sending metrics.
+ACCESS_CACHE_FAILURE = 101
+ACCESS_HISTORY_FAILURE = 102
+IMPORT_FAILURE = 103
+MLOCATEDB_LOCKED = 104
+
+# Test finder constants.
+MODULE_CONFIG = 'AndroidTest.xml'
+MODULE_COMPATIBILITY_SUITES = 'compatibility_suites'
+MODULE_NAME = 'module_name'
+MODULE_PATH = 'path'
+MODULE_CLASS = 'class'
+MODULE_INSTALLED = 'installed'
+MODULE_CLASS_ROBOLECTRIC = 'ROBOLECTRIC'
+MODULE_CLASS_NATIVE_TESTS = 'NATIVE_TESTS'
+MODULE_CLASS_JAVA_LIBRARIES = 'JAVA_LIBRARIES'
+MODULE_TEST_CONFIG = 'test_config'
+
+# Env constants
+ANDROID_BUILD_TOP = 'ANDROID_BUILD_TOP'
+ANDROID_OUT = 'OUT'
+ANDROID_OUT_DIR = 'OUT_DIR'
+ANDROID_HOST_OUT = 'ANDROID_HOST_OUT'
+ANDROID_PRODUCT_OUT = 'ANDROID_PRODUCT_OUT'
+
+# Test Info data keys
+# Value of include-filter option.
+TI_FILTER = 'filter'
+TI_REL_CONFIG = 'rel_config'
+TI_MODULE_CLASS = 'module_class'
+# Value of module-arg option
+TI_MODULE_ARG = 'module-arg'
+
+# Google TF
+GTF_MODULE = 'google-tradefed'
+GTF_TARGET = 'google-tradefed-core'
+
+# Test group for tests in TEST_MAPPING
+TEST_GROUP_PRESUBMIT = 'presubmit'
+TEST_GROUP_POSTSUBMIT = 'postsubmit'
+TEST_GROUP_ALL = 'all'
+# Key in TEST_MAPPING file for a list of imported TEST_MAPPING file
+TEST_MAPPING_IMPORTS = 'imports'
+
+# TradeFed command line args
+TF_INCLUDE_FILTER_OPTION = 'include-filter'
+TF_EXCLUDE_FILTER_OPTION = 'exclude-filter'
+TF_INCLUDE_FILTER = '--include-filter'
+TF_EXCLUDE_FILTER = '--exclude-filter'
+TF_ATEST_INCLUDE_FILTER = '--atest-include-filter'
+TF_ATEST_INCLUDE_FILTER_VALUE_FMT = '{test_name}:{test_filter}'
+TF_MODULE_ARG = '--module-arg'
+TF_MODULE_ARG_VALUE_FMT = '{test_name}:{option_name}:{option_value}'
+TF_SUITE_FILTER_ARG_VALUE_FMT = '"{test_name} {option_value}"'
+TF_SKIP_LOADING_CONFIG_JAR = '--skip-loading-config-jar'
+
+# Suite Plans
+SUITE_PLANS = frozenset(['cts'])
+
+# Constants used for AtestArgParser
+HELP_DESC = 'Build, install and run Android tests locally.'
+BUILD_STEP = 'build'
+INSTALL_STEP = 'install'
+TEST_STEP = 'test'
+ALL_STEPS = [BUILD_STEP, INSTALL_STEP, TEST_STEP]
+REBUILD_MODULE_INFO_FLAG = '--rebuild-module-info'
+
+# ANSI code shift for colorful print
+BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
+
+# Answers equivalent to YES!
+AFFIRMATIVES = ['y', 'Y', 'yes', 'Yes', 'YES', '']
+LD_RANGE = 2
+
+# Types of Levenshetine Distance Cost
+COST_TYPO = (1, 1, 1)
+COST_SEARCH = (8, 1, 5)
+
+# Value of TestInfo install_locations.
+DEVICELESS_TEST = 'host'
+DEVICE_TEST = 'device'
+BOTH_TEST = 'both'
+
+# Metrics
+METRICS_URL = 'http://asuite-218222.appspot.com/atest/metrics'
+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/',
+    'EXTERNAL': 'https://opensource.google.com/docs/cla/'
+}
+PRIVACY_POLICY_URL = 'https://policies.google.com/privacy'
+TERMS_SERVICE_URL = 'https://policies.google.com/terms'
+TOOL_NAME = 'atest'
+TF_PREPARATION = 'tf-preparation'
+
+# 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'
+
+# TreeHugger TEST_MAPPING SUITE_PLANS
+TEST_MAPPING_SUITES = ['device-tests', 'general-tests']
+
+# VTS TF
+VTS_TF_MODULE = 'vts-tradefed'
+
+# ATest TF
+ATEST_TF_MODULE = 'atest-tradefed'
+
+# Build environment variable for each build on ATest
+# With SOONG_COLLECT_JAVA_DEPS enabled, out/soong/module_bp_java_deps.json will
+# be generated when make.
+ATEST_BUILD_ENV = {'SOONG_COLLECT_JAVA_DEPS':'true'}
+
+# For generating dependencies in module-info.json, appending deps-license in the
+# make command is a must. Also the environment variables PROJ_PATH and DEP_PATH
+# are necessary.
+DEPS_LICENSE = 'deps-license'
+DEPS_LICENSE_ENV = {'PROJ_PATH': '.', 'DEP_PATH': '.'}
+
+# Atest index path and relative dirs/caches.
+INDEX_DIR = os.path.join(os.getenv(ANDROID_HOST_OUT, ''), 'indexes')
+LOCATE_CACHE = os.path.join(INDEX_DIR, 'mlocate.db')
+INT_INDEX = os.path.join(INDEX_DIR, 'integration.idx')
+CLASS_INDEX = os.path.join(INDEX_DIR, 'classes.idx')
+CC_CLASS_INDEX = os.path.join(INDEX_DIR, 'cc_classes.idx')
+PACKAGE_INDEX = os.path.join(INDEX_DIR, 'packages.idx')
+QCLASS_INDEX = os.path.join(INDEX_DIR, 'fqcn.idx')
+MODULE_INDEX = os.path.join(INDEX_DIR, 'modules.idx')
+VERSION_FILE = os.path.join(os.path.dirname(__file__), 'VERSION')
+
+# Regeular Expressions
+CC_EXT_RE = re.compile(r'.*\.(cc|cpp)$')
+JAVA_EXT_RE = re.compile(r'.*\.(java|kt)$')
+# e.g. /path/to/ccfile.cc: TEST_F(test_name, method_name){
+CC_OUTPUT_RE = re.compile(r'(?P<file_path>/.*):\s*TEST(_F|_P)?[ ]*\('
+                          r'(?P<test_name>\w+)\s*,\s*(?P<method_name>\w+)\)'
+                          r'\s*\{')
+CC_GREP_RE = r'^[ ]*TEST(_P|_F)?[ ]*\([[:alnum:]].*,'
+# e.g. /path/to/Javafile.java:package com.android.settings.accessibility
+# grab the path, Javafile(class) and com.android.settings.accessibility(package)
+CLASS_OUTPUT_RE = re.compile(r'(?P<java_path>.*/(?P<class>[A-Z]\w+)\.\w+)[:].*')
+QCLASS_OUTPUT_RE = re.compile(r'(?P<java_path>.*/(?P<class>[A-Z]\w+)\.\w+)'
+                              r'[:]\s*package\s+(?P<package>[^(;|\s)]+)\s*')
+PACKAGE_OUTPUT_RE = re.compile(r'(?P<java_dir>/.*/).*[.](java|kt)[:]\s*package\s+'
+                               r'(?P<package>[^(;|\s)]+)\s*')
+
+ATEST_RESULT_ROOT = '/tmp/atest_result'
diff --git a/atest/docs/atest_structure.md b/atest/docs/atest_structure.md
new file mode 100644
index 0000000..1ff7b90
--- /dev/null
+++ b/atest/docs/atest_structure.md
@@ -0,0 +1,116 @@
+# Atest Developer Guide
+
+You're here because you'd like to contribute to atest. To start off, we'll
+explain how atest is structured and where the major pieces live and what they
+do. If you're more interested in how to use atest, go to the [README](../README.md).
+
+##### Table of Contents
+1. [Overall Structure](#overall-structure)
+2. [Major Files and Dirs](#major-files-and-dirs)
+3. [Test Finders](#test-finders)
+4. [Test Runners](#test-runners)
+5. [Constants Override](#constants-override)
+
+## <a name="overall-structure">Overall Structure</a>
+
+Atest is primarily composed of 2 components: [test finders](#test-finders) and
+[test runners](#test-runners). At a high level, atest does the following:
+1. Parse args and verify environment
+2. Find test(s) based on user input
+3. Build test dependencies
+4. Run test(s)
+
+Let's walk through an example run and highlight what happens under the covers.
+
+> ```# atest hello_world_test```
+
+Atest will first check the environment is setup and then load up the
+module-info.json file (and build it if it's not detected or we want to rebuild
+it). That is a critical piece that atest depends on. Module-info.json contains a
+list of all modules in the android repo and some relevant info (e.g.
+compatibility_suite, auto_gen_config, etc) that is used during the test finding
+process. We create the results dir for our test runners to dump results in and
+proceed to the first juicy part of atest, finding tests.
+
+The tests specified by the user are passed into the ```CLITranslator``` to
+transform the user input into a ```TestInfo``` object that contains all of the
+required and optional bits used to run the test as how the user intended.
+Required info would be the test name, test dependencies, and the test runner
+used to run the test. Optional bits would be additional args for the test and
+method/class filters.
+
+Once ```TestInfo``` objects have been constructed for all the tests passed in
+by the user, all of the test dependencies are built. This step can by bypassed
+if the user specifies only _-t_ or _-i_.
+
+The final step is to run the tests which is where the test runners do their job.
+All of the ```TestInfo``` objects get passed into the ```test_runner_handler```
+which invokes each ```TestInfo``` specified test runner. In this specific case,
+the ```AtestTradefedTestRunner``` is used to kick off ```hello_world_test```.
+
+Read on to learn more about the classes mentioned.
+
+## <a name="major-files-and-dirs">Major Files and Dirs</a>
+
+Here is a list of major files and dirs that are important to point out:
+* ```atest.py``` - Main entry point.
+* ```cli_translator.py``` - Home of ```CLITranslator``` class. Translates the
+  user input into something the test runners can understand.
+* ```test_finder_handler.py``` - Module that collects all test finders,
+  determines which test finder methods to use and returns them for
+  ```CLITranslator``` to utilize.
+* ```test_finders/``` - Location of test finder classes. More details on test
+  finders [below](#test-finders).
+* ```test_finders/test_info.py``` - Module that defines ```TestInfo``` class.
+* ```test_runner_handler.py``` - Module that collects all test runners and
+  contains logic to determine what test runner to use for a particular
+  ```TestInfo```.
+* ```test_runners/``` - Location of test runner classes. More details on test
+  runners [below](#test-runners).
+* ```constants_default.py``` - Location of constant defaults. Need to override
+  some of these constants for your private repo? [Instructions below](#constants-override).
+
+## <a name="test-finders">Test Finders</a>
+
+Test finders are classes that host find methods. The find methods are called by
+atest to find tests in the android repo based on the user's input (path,
+filename, class, etc).  Find methods will also find the corresponding test
+dependencies for the supplied test, translating it into a form that a test
+runner can understand, and specifying the test runner.
+
+For more details and instructions on how to create new test finders,
+[go here](./develop_test_finders.md)
+
+## <a name="test-runners">Test Runners</a>
+
+Test Runners are classes that execute the tests. They consume a ```TestInfo```
+and execute the test as specified.
+
+For more details and instructions on how to create new test runners, [go here](./develop_test_runners.md)
+
+## <a name="constants-override">Constants Override</a>
+
+You'd like to override some constants but not sure how?  Override them with your
+own constants_override.py that lives in your own private repo.
+
+1. Create new ```constants_override.py``` (or whatever you'd like to name it) in
+  your own repo. It can live anywhere but just for example purposes, let's
+  specify the path to be ```<private repo>/path/to/constants_override/constants_override.py```.
+2. Add a ```vendorsetup.sh``` script in ```//vendor/<somewhere>``` to export the
+  path of ```constants_override.py``` base path into ```PYTHONPATH```.
+```bash
+# This file is executed by build/envsetup.sh
+_path_to_constants_override="$(gettop)/path/to/constants_override"
+if [[ ! $PYTHONPATH == *${_path_to_constants_override}* ]]; then
+  export PYTHONPATH=${_path_to_constants_override}:$PYTHONPATH
+fi
+```
+3. Try-except import ```constants_override``` in ```constants.py```.
+```python
+try:
+    from constants_override import *
+except ImportError:
+    pass
+```
+4. You're done! To pick up the override, rerun build/envsetup.sh to kick off the
+  vendorsetup.sh script.
diff --git a/atest/docs/develop_test_finders.md b/atest/docs/develop_test_finders.md
new file mode 100644
index 0000000..5235ef7
--- /dev/null
+++ b/atest/docs/develop_test_finders.md
@@ -0,0 +1,64 @@
+# Test Finder Developer Guide
+
+Learn about test finders and how to create a new test finder class.
+
+##### Table of Contents
+1. [Test Finder Details](#test-finder-details)
+2. [Creating a Test Finder](#creating-a-test-finder)
+
+## <a name="test-finder-details">Test Finder Details</a>
+
+A test finder class holds find methods. A find method is given a string (the
+user input) and should try to resolve that string into a ```TestInfo``` object.
+A ```TestInfo``` object holds the test name, test dependencies, test runner, and
+a data field to hold misc bits like filters and extra args for the test.  The
+test finder class can hold multiple find methods. The find methods are grouped
+together in a class so they can share metadata for optimal test finding.
+Examples of metadata would be the ```ModuleInfo``` object or the dirs that hold
+the test configs for the ```TFIntegrationFinder```.
+
+**When should I create a new test finder class?**
+
+If the metadata used to find a test is unlike existing test finder classes,
+that is the right time to create a new class. Metadata can be anything like
+file name patterns, a special file in a dir to indicate it's a test, etc. The
+default test finder classes use the module-info.json and specific dir paths
+metadata (```ModuleFinder``` and ```TFIntegrationFinder``` respectively).
+
+## <a name="creating-a-test-finder">Creating a Test Finder</a>
+
+First thing to choose is where to put the test finder. This will primarily
+depend on if the test finder will be public or private. If public,
+```test_finders/``` is the default location.
+
+> If it will be private, then you can
+> follow the same instructions for ```vendorsetup.sh``` in
+> [constants override](atest_structure.md#constants-override) where you will
+> add the path of where the test finder lives into ```$PYTHONPATH```. Same
+> rules apply, rerun ```build/envsetup.sh``` to update ```$PYTHONPATH```.
+
+Now define your class and decorate it with the
+```test_finder_base.find_method_register``` decorator. This decorator will
+create a list of find methods that ```test_finder_handler``` will use to collect
+the find methods from your test finder class. Take a look at
+```test_finders/example_test_finder.py``` as an example.
+
+Define the find methods in your test finder class. These find methods must
+return a ```TestInfo``` object. Extra bits of info can be stored in the data
+field as a dict.  Check out ```ExampleFinder``` to see how the data field is
+used.
+
+Decorate each find method with the ```test_finder_base.register``` decorator.
+This is used by the class decorator to identify the find methods of the class.
+
+Final bit is to add your test finder class to ```test_finder_handler```.
+Try-except import it in the ```_get_test_finders``` method and that should be
+it. The find methods will be collected and executed before the default find
+methods.
+```python
+try:
+    from test_finders import new_test_finder
+    test_finders_list.add(new_test_finder.NewTestFinder)
+except ImportError:
+    pass
+```
diff --git a/atest/docs/develop_test_runners.md b/atest/docs/develop_test_runners.md
new file mode 100644
index 0000000..80388ac
--- /dev/null
+++ b/atest/docs/develop_test_runners.md
@@ -0,0 +1,64 @@
+# Test Runner Developer Guide
+
+Learn about test runners and how to create a new test runner class.
+
+##### Table of Contents
+1. [Test Runner Details](#test-runner-details)
+2. [Creating a Test Runner](#creating-a-test-runner)
+
+## <a name="test-runner-details">Test Runner Details</a>
+
+The test runner class is responsible for test execution. Its primary logic
+involve construction of the commandline given a ```TestInfo``` and
+```extra_args``` passed into the ```run_tests``` method. The extra_args are
+top-level args consumed by atest passed onto the test runner. It is up to the
+test runner to translate those args into the specific args the test runner
+accepts. In this way, you can think of the test runner as a translator between
+the atest CLI and your test runner's CLI. The reason for this is so that atest
+can have a consistent CLI for args instead of requiring the users to remember
+the differing CLIs of various test runners.  The test runner should also
+determine its specific dependencies that need to be built prior to any test
+execution.
+
+## <a name="creating-a-test-runner">Creating a Test Runner</a>
+
+First thing to choose is where to put the test runner. This will primarily
+depend on if the test runner will be public or private. If public,
+```test_runners/``` is the default location.
+
+> If it will be private, then you can
+> follow the same instructions for ```vendorsetup.sh``` in
+> [constants override](atest_structure.md#constants-override) where you will
+> add the path of where the test runner lives into ```$PYTHONPATH```. Same
+> rules apply, rerun ```build/envsetup.sh``` to update ```$PYTHONPATH```.
+
+To create a new test runner, create a new class that inherits
+```TestRunnerBase```. Take a look at ```test_runners/example_test_runner.py```
+to see what a simple test runner will look like.
+
+**Important Notes**
+You'll need to override the following parent methods:
+* ```host_env_check()```: Check if host environment is properly setup for the
+  test runner. Raise an expception if not.
+* ```get_test_runner_build_reqs()```: Return a set of build targets that need
+  to be built prior to test execution.
+* ```run_tests()```: Execute the test(s).
+
+And define the following class vars:
+* ```NAME```: Unique name of the test runner.
+* ```EXECUTABLE```: Test runner command, should be an absolute path if the
+  command can not be found in ```$PATH```.
+
+There is a parent helper method (```run```) that should be used to execute the
+actual test command.
+
+Once the test runner class is created, you'll need to add it in
+```test_runner_handler``` so that atest is aware of it. Try-except import the
+test runner in ```_get_test_runners``` like how ```ExampleTestRunner``` is.
+```python
+try:
+    from test_runners import new_test_runner
+    test_runners_dict[new_test_runner.NewTestRunner.NAME] = new_test_runner.NewTestRunner
+except ImportError:
+    pass
+```
diff --git a/atest/docs/developer_workflow.md b/atest/docs/developer_workflow.md
new file mode 100644
index 0000000..578ec1b
--- /dev/null
+++ b/atest/docs/developer_workflow.md
@@ -0,0 +1,154 @@
+# Atest Developer Workflow
+
+This document explains the practical steps for contributing code to atest.
+
+##### Table of Contents
+1. [Identify the code you should work on](#identify-the-code-you-should-work-on)
+2. [Working on the Python Code](#working-on-the-python-code)
+3. [Working on the TradeFed Code](#working-on-the-tradefed-code)
+4. [Working on the VTS-TradeFed Code](#working-on-the-vts-tradefed-code)
+5. [Working on the Robolectric Code](#working-on-the-robolectric-code)
+
+
+## <a name="what-code">Identify the code you should work on</a>
+
+Atest is essentially a wrapper around various test runners. Because of
+this division, your first step should be to identify the code
+involved with your change. This will help determine what tests you write
+and run.  Note that the wrapper code is written in python, so we'll be
+referring to it as the "Python Code".
+
+##### The Python Code
+
+This code defines atest's command line interface.
+Its job is to translate user inputs into (1) build targets and (2)
+information needed for the test runner to run the test. It then invokes
+the appropriate test runner code to run the tests. As the tests
+are run it also parses the test runner's output into the output seen by
+the user. It uses Test Finder and Test Runner classes to do this work.
+If your contribution involves any of this functionality, this is the
+code you'll want to work on.
+
+<p>For more details on how this code works, checkout the following docs:
+
+ - [General Structure](./atest_structure.md)
+ - [Test Finders](./develop_test_finders.md)
+ - [Test Runners](./develop_test_runners.md)
+
+##### The Test Runner Code
+
+This is the code that actually runs the test. If your change
+involves how the test is actually run, you'll need to work with this
+code.
+
+Each test runner will have a different workflow. Atest currently
+supports the following test runners:
+- TradeFed
+- VTS-TradeFed
+- Robolectric
+
+
+## <a name="working-on-the-python-code">Working on the Python Code</a>
+
+##### Where does the Python code live?
+
+The python code lives here: `tools/tradefederation/core/atest/`
+(path relative to android repo root)
+
+##### Writing tests
+
+Test files go in the same directory as the file being tested. The test
+file should have the same name as the file it's testing, except it
+should have "_unittests" appended to the name. For example, tests
+for the logic in `cli_translator.py` go in the file
+`cli_translator_unittests.py` in the same directory.
+
+
+##### Running tests
+
+Python tests are just python files executable by the Python interpreter.
+You can run ALL the python tests by executing this bash script in the
+atest root dir: `./run_atest_unittests.sh`. Alternatively, you can
+directly execute any individual unittest file. However, you'll need to
+first add atest to your PYTHONPATH via entering in your terminal:
+`PYTHONPATH=<atest_dir>:$PYTHONPATH`.
+
+All tests should be passing before you submit your change.
+
+## <a name="working-on-the-tradefed-code">Working on the TradeFed Code</a>
+
+##### Where does the TradeFed code live?
+
+The TradeFed code lives here:
+`tools/tradefederation/core/src/com/android/tradefed/` (path relative
+to android repo root).
+
+The `testtype/suite/AtestRunner.java` is the most important file in
+the TradeFed Code. It defines the TradeFed API used
+by the Python Code, specifically by
+`test_runners/atest_tf_test_runner.py`. This is the file you'll want
+to edit if you need to make changes to the TradeFed code.
+
+
+##### Writing tests
+
+Tradefed test files live in a parallel `/tests/` file tree here:
+`tools/tradefederation/core/tests/src/com/android/tradefed/`.
+A test file should have the same name as the file it's testing,
+except with the word "Test" appended to the end. <p>
+For example, the tests for `tools/tradefederation/core/src/com/android/tradefed/testtype/suite/AtestRunner.java`
+can be found in `tools/tradefederation/core/tests/src/com/android/tradefed/testtype/suite/AtestRunnerTest.java`.
+
+##### Running tests
+
+TradeFed itself is used to run the TradeFed unittests so you'll need
+to build TradeFed first. See the
+[TradeFed README](../../README.md) for information about setting up
+TradeFed. <p>
+There are so many TradeFed tests that you'll probably want to
+first run the test file your code change affected individually. The
+command to run an individual test file is:<br>
+
+`tradefed.sh run host -n --class <fully.qualified.ClassName>`
+
+Thus, to run all the tests in AtestRunnerTest.java, you'd enter:
+
+`tradefed.sh run host -n --class com.android.tradefed.testtype.suite.AtestRunnerTest`
+
+To run ALL the TradeFed unittests, enter:
+`./tools/tradefederation/core/tests/run_tradefed_tests.sh`
+(from android repo root)
+
+Before submitting code you should run all the TradeFed tests.
+
+## <a name="working-on-the-vts-tradefed-code">Working on the VTS-TradeFed Code</a>
+
+##### Where does the VTS-TradeFed code live?
+
+The VTS-Tradefed code lives here: `test/vts/tools/vts-tradefed/`
+(path relative to android repo root)
+
+##### Writing tests
+
+You shouldn't need to edit vts-tradefed code, so there is no
+need to write vts-tradefed tests. Reach out to the vts team
+if you need information on their unittests.
+
+##### Running tests
+
+Again, you shouldn't need to change vts-tradefed code.
+
+## <a name="working-on-the-robolectric-code">Working on the Robolectric Code</a>
+
+##### Where does the Robolectric code live?
+
+The Robolectric code lives here: `prebuilts/misc/common/robolectric/3.6.1/`
+(path relative to android repo root)
+
+##### Writing tests
+
+You shouldn't need to edit this code, so no need to write tests.
+
+##### Running tests
+
+Again, you shouldn't need to edit this code, so no need to run test.
diff --git a/atest/metrics/__init__.py b/atest/metrics/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/metrics/__init__.py
diff --git a/atest/metrics/clearcut_client.py b/atest/metrics/clearcut_client.py
new file mode 100644
index 0000000..ecb83c3
--- /dev/null
+++ b/atest/metrics/clearcut_client.py
@@ -0,0 +1,176 @@
+# Copyright 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.
+
+"""Python client library to write logs to Clearcut.
+
+This class is intended to be general-purpose, usable for any Clearcut LogSource.
+
+    Typical usage example:
+
+    client = clearcut.Clearcut(clientanalytics_pb2.LogRequest.MY_LOGSOURCE)
+    client.log(my_event)
+    client.flush_events()
+"""
+
+import logging
+import threading
+import time
+try:
+    # PYTHON2
+    from urllib2 import urlopen
+    from urllib2 import Request
+    from urllib2 import HTTPError
+    from urllib2 import URLError
+except ImportError:
+    # PYTHON3
+    from urllib.request import urlopen
+    from urllib.request import Request
+    from urllib.request import HTTPError
+    from urllib.request import URLError
+
+from proto import clientanalytics_pb2
+
+_CLEARCUT_PROD_URL = 'https://play.googleapis.com/log'
+_DEFAULT_BUFFER_SIZE = 100  # Maximum number of events to be buffered.
+_DEFAULT_FLUSH_INTERVAL_SEC = 60  # 1 Minute.
+_BUFFER_FLUSH_RATIO = 0.5  # Flush buffer when we exceed this ratio.
+_CLIENT_TYPE = 6
+
+class Clearcut(object):
+    """Handles logging to Clearcut."""
+
+    def __init__(self, log_source, url=None, buffer_size=None,
+                 flush_interval_sec=None):
+        """Initializes a Clearcut client.
+
+        Args:
+            log_source: The log source.
+            url: The Clearcut url to connect to.
+            buffer_size: The size of the client buffer in number of events.
+            flush_interval_sec: The flush interval in seconds.
+        """
+        self._clearcut_url = url if url else _CLEARCUT_PROD_URL
+        self._log_source = log_source
+        self._buffer_size = buffer_size if buffer_size else _DEFAULT_BUFFER_SIZE
+        self._pending_events = []
+        if flush_interval_sec:
+            self._flush_interval_sec = flush_interval_sec
+        else:
+            self._flush_interval_sec = _DEFAULT_FLUSH_INTERVAL_SEC
+        self._pending_events_lock = threading.Lock()
+        self._scheduled_flush_thread = None
+        self._scheduled_flush_time = float('inf')
+        self._min_next_request_time = 0
+
+    def log(self, event):
+        """Logs events to Clearcut.
+
+        Logging an event can potentially trigger a flush of queued events. Flushing
+        is triggered when the buffer is more than half full or after the flush
+        interval has passed.
+
+        Args:
+          event: A LogEvent to send to Clearcut.
+        """
+        self._append_events_to_buffer([event])
+
+    def flush_events(self):
+        """ Cancel whatever is scheduled and schedule an immediate flush."""
+        if self._scheduled_flush_thread:
+            self._scheduled_flush_thread.cancel()
+        self._min_next_request_time = 0
+        self._schedule_flush_thread(0)
+
+    def _serialize_events_to_proto(self, events):
+        log_request = clientanalytics_pb2.LogRequest()
+        log_request.request_time_ms = int(time.time() * 1000)
+        # pylint: disable=no-member
+        log_request.client_info.client_type = _CLIENT_TYPE
+        log_request.log_source = self._log_source
+        log_request.log_event.extend(events)
+        return log_request
+
+    def _append_events_to_buffer(self, events, retry=False):
+        with self._pending_events_lock:
+            self._pending_events.extend(events)
+            if len(self._pending_events) > self._buffer_size:
+                index = len(self._pending_events) - self._buffer_size
+                del self._pending_events[:index]
+            self._schedule_flush(retry)
+
+    def _schedule_flush(self, retry):
+        if (not retry
+                and len(self._pending_events) >= int(self._buffer_size *
+                                                     _BUFFER_FLUSH_RATIO)
+                and self._scheduled_flush_time > time.time()):
+            # Cancel whatever is scheduled and schedule an immediate flush.
+            if self._scheduled_flush_thread:
+                self._scheduled_flush_thread.cancel()
+            self._schedule_flush_thread(0)
+        elif self._pending_events and not self._scheduled_flush_thread:
+            # Schedule a flush to run later.
+            self._schedule_flush_thread(self._flush_interval_sec)
+
+    def _schedule_flush_thread(self, time_from_now):
+        min_wait_sec = self._min_next_request_time - time.time()
+        if min_wait_sec > time_from_now:
+            time_from_now = min_wait_sec
+        logging.debug('Scheduling thread to run in %f seconds', time_from_now)
+        self._scheduled_flush_thread = threading.Timer(time_from_now, self._flush)
+        self._scheduled_flush_time = time.time() + time_from_now
+        self._scheduled_flush_thread.start()
+
+    def _flush(self):
+        """Flush buffered events to Clearcut.
+
+        If the sent request is unsuccessful, the events will be appended to
+        buffer and rescheduled for next flush.
+        """
+        with self._pending_events_lock:
+            self._scheduled_flush_time = float('inf')
+            self._scheduled_flush_thread = None
+            events = self._pending_events
+            self._pending_events = []
+        if self._min_next_request_time > time.time():
+            self._append_events_to_buffer(events, retry=True)
+            return
+        log_request = self._serialize_events_to_proto(events)
+        self._send_to_clearcut(log_request.SerializeToString())
+
+    #pylint: disable=broad-except
+    def _send_to_clearcut(self, data):
+        """Sends a POST request with data as the body.
+
+        Args:
+            data: The serialized proto to send to Clearcut.
+        """
+        request = Request(self._clearcut_url, data=data)
+        try:
+            response = urlopen(request)
+            msg = response.read()
+            logging.debug('LogRequest successfully sent to Clearcut.')
+            log_response = clientanalytics_pb2.LogResponse()
+            log_response.ParseFromString(msg)
+            # pylint: disable=no-member
+            # Throttle based on next_request_wait_millis value.
+            self._min_next_request_time = (log_response.next_request_wait_millis
+                                           / 1000 + time.time())
+            logging.debug('LogResponse: %s', log_response)
+        except HTTPError as e:
+            logging.debug('Failed to push events to Clearcut. Error code: %d',
+                          e.code)
+        except URLError:
+            logging.debug('Failed to push events to Clearcut.')
+        except Exception as e:
+            logging.debug(e)
diff --git a/atest/metrics/metrics.py b/atest/metrics/metrics.py
new file mode 100644
index 0000000..f6446a6
--- /dev/null
+++ b/atest/metrics/metrics.py
@@ -0,0 +1,148 @@
+# Copyright 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.
+
+"""
+Metrics class.
+"""
+
+import constants
+
+from . import metrics_base
+
+class AtestStartEvent(metrics_base.MetricsBase):
+    """
+    Create Atest start event and send to clearcut.
+
+    Usage:
+        metrics.AtestStartEvent(
+            command_line='example_atest_command',
+            test_references=['example_test_reference'],
+            cwd='example/working/dir',
+            os='example_os')
+    """
+    _EVENT_NAME = 'atest_start_event'
+    command_line = constants.INTERNAL
+    test_references = constants.INTERNAL
+    cwd = constants.INTERNAL
+    os = constants.INTERNAL
+
+class AtestExitEvent(metrics_base.MetricsBase):
+    """
+    Create Atest exit event and send to clearcut.
+
+    Usage:
+        metrics.AtestExitEvent(
+            duration=metrics_utils.convert_duration(end-start),
+            exit_code=0,
+            stacktrace='some_trace',
+            logs='some_logs')
+    """
+    _EVENT_NAME = 'atest_exit_event'
+    duration = constants.EXTERNAL
+    exit_code = constants.EXTERNAL
+    stacktrace = constants.INTERNAL
+    logs = constants.INTERNAL
+
+class FindTestFinishEvent(metrics_base.MetricsBase):
+    """
+    Create find test finish event and send to clearcut.
+
+    Occurs after a SINGLE test reference has been resolved to a test or
+    not found.
+
+    Usage:
+        metrics.FindTestFinishEvent(
+            duration=metrics_utils.convert_duration(end-start),
+            success=true,
+            test_reference='hello_world_test',
+            test_finders=['example_test_reference', 'ref2'],
+            test_info="test_name: hello_world_test -
+                test_runner:AtestTradefedTestRunner -
+                build_targets:
+                    set(['MODULES-IN-platform_testing-tests-example-native']) -
+                data:{'rel_config':
+                    'platform_testing/tests/example/native/AndroidTest.xml',
+                    'filter': frozenset([])} -
+                suite:None - module_class: ['NATIVE_TESTS'] -
+                install_locations:set(['device', 'host'])")
+    """
+    _EVENT_NAME = 'find_test_finish_event'
+    duration = constants.EXTERNAL
+    success = constants.EXTERNAL
+    test_reference = constants.INTERNAL
+    test_finders = constants.INTERNAL
+    test_info = constants.INTERNAL
+
+class BuildFinishEvent(metrics_base.MetricsBase):
+    """
+    Create build finish event and send to clearcut.
+
+    Occurs after the build finishes, either successfully or not.
+
+    Usage:
+        metrics.BuildFinishEvent(
+            duration=metrics_utils.convert_duration(end-start),
+            success=true,
+            targets=['target1', 'target2'])
+    """
+    _EVENT_NAME = 'build_finish_event'
+    duration = constants.EXTERNAL
+    success = constants.EXTERNAL
+    targets = constants.INTERNAL
+
+class RunnerFinishEvent(metrics_base.MetricsBase):
+    """
+    Create run finish event and send to clearcut.
+
+    Occurs when a single test runner has completed.
+
+    Usage:
+        metrics.RunnerFinishEvent(
+            duration=metrics_utils.convert_duration(end-start),
+            success=true,
+            runner_name='AtestTradefedTestRunner'
+            test=[{name:'hello_world_test', result:0, stacktrace:''},
+                  {name:'test2', result:1, stacktrace:'xxx'}])
+    """
+    _EVENT_NAME = 'runner_finish_event'
+    duration = constants.EXTERNAL
+    success = constants.EXTERNAL
+    runner_name = constants.EXTERNAL
+    test = constants.INTERNAL
+
+class RunTestsFinishEvent(metrics_base.MetricsBase):
+    """
+    Create run tests finish event and send to clearcut.
+
+    Occurs after all test runners and tests have finished.
+
+    Usage:
+        metrics.RunTestsFinishEvent(
+            duration=metrics_utils.convert_duration(end-start))
+    """
+    _EVENT_NAME = 'run_tests_finish_event'
+    duration = constants.EXTERNAL
+
+class LocalDetectEvent(metrics_base.MetricsBase):
+    """
+    Create local detection event and send it to clearcut.
+
+    Usage:
+        metrics.LocalDetectEvent(
+            detect_type=0,
+            result=0)
+    """
+    _EVENT_NAME = 'local_detect_event'
+    detect_type = constants.EXTERNAL
+    result = constants.EXTERNAL
diff --git a/atest/metrics/metrics_base.py b/atest/metrics/metrics_base.py
new file mode 100644
index 0000000..73027b4
--- /dev/null
+++ b/atest/metrics/metrics_base.py
@@ -0,0 +1,145 @@
+# Copyright 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.
+
+"""
+Metrics base class.
+"""
+
+from __future__ import print_function
+
+import logging
+import random
+import socket
+import subprocess
+import time
+import uuid
+
+import asuite_metrics
+import constants
+
+from proto import clientanalytics_pb2
+from proto import external_user_log_pb2
+from proto import internal_user_log_pb2
+
+from . import clearcut_client
+
+INTERNAL_USER = 0
+EXTERNAL_USER = 1
+
+ATEST_EVENTS = {
+    INTERNAL_USER: internal_user_log_pb2.AtestLogEventInternal,
+    EXTERNAL_USER: external_user_log_pb2.AtestLogEventExternal
+}
+# log source
+ATEST_LOG_SOURCE = {
+    INTERNAL_USER: 971,
+    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."""
+
+    _run_id = str(uuid.uuid4())
+    try:
+        #pylint: disable=protected-access
+        _user_key = str(asuite_metrics._get_grouping_key())
+    #pylint: disable=broad-except
+    except Exception:
+        _user_key = asuite_metrics.DUMMY_UUID
+    _user_type = get_user_type()
+    _log_source = ATEST_LOG_SOURCE[_user_type]
+    cc = clearcut_client.Clearcut(_log_source)
+    tool_name = None
+
+    def __new__(cls, **kwargs):
+        """Send metric event to clearcut.
+
+        Args:
+            cls: this class object.
+            **kwargs: A dict of named arguments.
+
+        Returns:
+            A Clearcut instance.
+        """
+        # pylint: disable=no-member
+        if not cls.tool_name:
+            logging.debug('There is no tool_name, and metrics stops sending.')
+            return None
+        allowed = ({constants.EXTERNAL} if cls._user_type == EXTERNAL_USER
+                   else {constants.EXTERNAL, constants.INTERNAL})
+        fields = [k for k, v in vars(cls).items()
+                  if not k.startswith('_') and v in allowed]
+        fields_and_values = {}
+        for field in fields:
+            if field in kwargs:
+                fields_and_values[field] = kwargs.pop(field)
+        params = {'user_key': cls._user_key,
+                  'run_id': cls._run_id,
+                  'user_type': cls._user_type,
+                  'tool_name': cls.tool_name,
+                  cls._EVENT_NAME: fields_and_values}
+        log_event = cls._build_full_event(ATEST_EVENTS[cls._user_type](**params))
+        cls.cc.log(log_event)
+        return cls.cc
+
+    @classmethod
+    def _build_full_event(cls, atest_event):
+        """This is all protobuf building you can ignore.
+
+        Args:
+            cls: this class object.
+            atest_event: A client_pb2.AtestLogEvent instance.
+
+        Returns:
+            A clientanalytics_pb2.LogEvent instance.
+        """
+        log_event = clientanalytics_pb2.LogEvent()
+        log_event.event_time_ms = int((time.time() - random.randint(1, 600)) * 1000)
+        log_event.source_extension = atest_event.SerializeToString()
+        return log_event
diff --git a/atest/metrics/metrics_utils.py b/atest/metrics/metrics_utils.py
new file mode 100644
index 0000000..a43b8f6
--- /dev/null
+++ b/atest/metrics/metrics_utils.py
@@ -0,0 +1,128 @@
+# Copyright 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.
+
+"""
+Utility functions for metrics.
+"""
+
+import os
+import platform
+import sys
+import time
+import traceback
+
+from . import metrics
+from . import metrics_base
+
+
+def static_var(varname, value):
+    """Decorator to cache static variable."""
+    def fun_var_decorate(func):
+        """Set the static variable in a function."""
+        setattr(func, varname, value)
+        return func
+    return fun_var_decorate
+
+
+@static_var("start_time", [])
+def get_start_time():
+    """Get start time.
+
+    Return:
+        start_time: Start time in seconds. Return cached start_time if exists,
+        time.time() otherwise.
+    """
+    if not get_start_time.start_time:
+        get_start_time.start_time = time.time()
+    return get_start_time.start_time
+
+
+def convert_duration(diff_time_sec):
+    """Compute duration from time difference.
+
+    A Duration represents a signed, fixed-length span of time represented
+    as a count of seconds and fractions of seconds at nanosecond
+    resolution.
+
+    Args:
+        dur_time_sec: The time in seconds as a floating point number.
+
+    Returns:
+        A dict of Duration.
+    """
+    seconds = int(diff_time_sec)
+    nanos = int((diff_time_sec - seconds)*10**9)
+    return {'seconds': seconds, 'nanos': nanos}
+
+
+# pylint: disable=broad-except
+def handle_exc_and_send_exit_event(exit_code):
+    """handle exceptions and send exit event.
+
+    Args:
+        exit_code: An integer of exit code.
+    """
+    stacktrace = logs = ''
+    try:
+        exc_type, exc_msg, _ = sys.exc_info()
+        stacktrace = traceback.format_exc()
+        if exc_type:
+            logs = '{etype}: {value}'.format(etype=exc_type.__name__,
+                                             value=exc_msg)
+    except Exception:
+        pass
+    send_exit_event(exit_code, stacktrace=stacktrace, logs=logs)
+
+
+def send_exit_event(exit_code, stacktrace='', logs=''):
+    """Log exit event and flush all events to clearcut.
+
+    Args:
+        exit_code: An integer of exit code.
+        stacktrace: A string of stacktrace.
+        logs: A string of logs.
+    """
+    clearcut = metrics.AtestExitEvent(
+        duration=convert_duration(time.time()-get_start_time()),
+        exit_code=exit_code,
+        stacktrace=stacktrace,
+        logs=logs)
+    # pylint: disable=no-member
+    if clearcut:
+        clearcut.flush_events()
+
+
+def send_start_event(tool_name, command_line='', test_references='',
+                     cwd=None, operating_system=None):
+    """Log start event of clearcut.
+
+    Args:
+        tool_name: A string of the asuite product name.
+        command_line: A string of the user input command.
+        test_references: A string of the input tests.
+        cwd: A string of current path.
+        operating_system: A string of user's operating system.
+    """
+    if not cwd:
+        cwd = os.getcwd()
+    if not operating_system:
+        operating_system = platform.platform()
+    # Without tool_name information, asuite's clearcut client will not send
+    # event to server.
+    metrics_base.MetricsBase.tool_name = tool_name
+    get_start_time()
+    metrics.AtestStartEvent(command_line=command_line,
+                            test_references=test_references,
+                            cwd=cwd,
+                            os=operating_system)
diff --git a/atest/module_info.py b/atest/module_info.py
new file mode 100644
index 0000000..ec9a7a9
--- /dev/null
+++ b/atest/module_info.py
@@ -0,0 +1,329 @@
+# Copyright 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.
+
+"""
+Module Info class used to hold cached module-info.json.
+"""
+
+import json
+import logging
+import os
+
+import atest_utils
+import constants
+
+# JSON file generated by build system that lists all buildable targets.
+_MODULE_INFO = 'module-info.json'
+
+
+class ModuleInfo(object):
+    """Class that offers fast/easy lookup for Module related details."""
+
+    def __init__(self, force_build=False, module_file=None):
+        """Initialize the ModuleInfo object.
+
+        Load up the module-info.json file and initialize the helper vars.
+
+        Args:
+            force_build: Boolean to indicate if we should rebuild the
+                         module_info file regardless if it's created or not.
+            module_file: String of path to file to load up. Used for testing.
+        """
+        module_info_target, name_to_module_info = self._load_module_info_file(
+            force_build, module_file)
+        self.name_to_module_info = name_to_module_info
+        self.module_info_target = module_info_target
+        self.path_to_module_info = self._get_path_to_module_info(
+            self.name_to_module_info)
+        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
+
+    @staticmethod
+    def _discover_mod_file_and_target(force_build):
+        """Find the module file.
+
+        Args:
+            force_build: Boolean to indicate if we should rebuild the
+                         module_info file regardless if it's created or not.
+
+        Returns:
+            Tuple of module_info_target and path to module file.
+        """
+        module_info_target = None
+        root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, '/')
+        out_dir = os.environ.get(constants.ANDROID_PRODUCT_OUT, root_dir)
+        module_file_path = os.path.join(out_dir, _MODULE_INFO)
+
+        # Check if the user set a custom out directory by comparing the out_dir
+        # to the root_dir.
+        if out_dir.find(root_dir) == 0:
+            # Make target is simply file path relative to root
+            module_info_target = os.path.relpath(module_file_path, root_dir)
+        else:
+            # If the user has set a custom out directory, generate an absolute
+            # path for module info targets.
+            logging.debug('User customized out dir!')
+            module_file_path = os.path.join(
+                os.environ.get(constants.ANDROID_PRODUCT_OUT), _MODULE_INFO)
+            module_info_target = module_file_path
+        if not os.path.isfile(module_file_path) or force_build:
+            logging.debug('Generating %s - this is required for '
+                          'initial runs.', _MODULE_INFO)
+            build_env = dict(constants.ATEST_BUILD_ENV)
+            build_env.update(constants.DEPS_LICENSE_ENV)
+            # Also build the deps-license module to generate dependencies data.
+            atest_utils.build([module_info_target, constants.DEPS_LICENSE],
+                              verbose=logging.getLogger().isEnabledFor(logging.DEBUG),
+                              env_vars=build_env)
+        return module_info_target, module_file_path
+
+    def _load_module_info_file(self, force_build, module_file):
+        """Load the module file.
+
+        Args:
+            force_build: Boolean to indicate if we should rebuild the
+                         module_info file regardless if it's created or not.
+            module_file: String of path to file to load up. Used for testing.
+
+        Returns:
+            Tuple of module_info_target and dict of json.
+        """
+        # If module_file is specified, we're testing so we don't care if
+        # module_info_target stays None.
+        module_info_target = None
+        file_path = module_file
+        if not file_path:
+            module_info_target, file_path = self._discover_mod_file_and_target(
+                force_build)
+        with open(file_path) as json_file:
+            mod_info = json.load(json_file)
+        return module_info_target, mod_info
+
+    @staticmethod
+    def _get_path_to_module_info(name_to_module_info):
+        """Return the path_to_module_info dict.
+
+        Args:
+            name_to_module_info: Dict of module name to module info dict.
+
+        Returns:
+            Dict of module path to module info dict.
+        """
+        path_to_module_info = {}
+        for mod_name, mod_info in name_to_module_info.items():
+            # Cross-compiled and multi-arch modules actually all belong to
+            # a single target so filter out these extra modules.
+            if mod_name != mod_info.get(constants.MODULE_NAME, ''):
+                continue
+            for path in mod_info.get(constants.MODULE_PATH, []):
+                mod_info[constants.MODULE_NAME] = mod_name
+                # There could be multiple modules in a path.
+                if path in path_to_module_info:
+                    path_to_module_info[path].append(mod_info)
+                else:
+                    path_to_module_info[path] = [mod_info]
+        return path_to_module_info
+
+    def is_module(self, name):
+        """Return True if name is a module, False otherwise."""
+        return name in self.name_to_module_info
+
+    def get_paths(self, name):
+        """Return paths of supplied module name, Empty list if non-existent."""
+        info = self.name_to_module_info.get(name)
+        if info:
+            return info.get(constants.MODULE_PATH, [])
+        return []
+
+    def get_module_names(self, rel_module_path):
+        """Get the modules that all have module_path.
+
+        Args:
+            rel_module_path: path of module in module-info.json
+
+        Returns:
+            List of module names.
+        """
+        return [m.get(constants.MODULE_NAME)
+                for m in self.path_to_module_info.get(rel_module_path, [])]
+
+    def get_module_info(self, mod_name):
+        """Return dict of info for given module name, None if non-existent."""
+        return self.name_to_module_info.get(mod_name)
+
+    def is_suite_in_compatibility_suites(self, suite, mod_info):
+        """Check if suite exists in the compatibility_suites of module-info.
+
+        Args:
+            suite: A string of suite name.
+            mod_info: Dict of module info to check.
+
+        Returns:
+            True if it exists in mod_info, False otherwise.
+        """
+        return suite in mod_info.get(constants.MODULE_COMPATIBILITY_SUITES, [])
+
+    def get_testable_modules(self, suite=None):
+        """Return the testable modules of the given suite name.
+
+        Args:
+            suite: A string of suite name. Set to None to return all testable
+            modules.
+
+        Returns:
+            List of testable modules. Empty list if non-existent.
+            If suite is None, return all the testable modules in module-info.
+        """
+        modules = set()
+        for _, info in self.name_to_module_info.items():
+            if self.is_testable_module(info):
+                if suite:
+                    if self.is_suite_in_compatibility_suites(suite, info):
+                        modules.add(info.get(constants.MODULE_NAME))
+                else:
+                    modules.add(info.get(constants.MODULE_NAME))
+        return modules
+
+    def is_testable_module(self, mod_info):
+        """Check if module is something we can test.
+
+        A module is testable if:
+          - it's installed, or
+          - it's a robolectric module (or shares path with one).
+
+        Args:
+            mod_info: Dict of module info to check.
+
+        Returns:
+            True if we can test this module, False otherwise.
+        """
+        if not mod_info:
+            return False
+        if mod_info.get(constants.MODULE_INSTALLED) and self.has_test_config(mod_info):
+            return True
+        if self.is_robolectric_test(mod_info.get(constants.MODULE_NAME)):
+            return True
+        return False
+
+    def has_test_config(self, mod_info):
+        """Validate if this module has a test config.
+
+        A module can have a test config in the following manner:
+          - AndroidTest.xml at the module path.
+          - test_config be set in module-info.json.
+          - Auto-generated config via the auto_test_config key in module-info.json.
+
+        Args:
+            mod_info: Dict of module info to check.
+
+        Returns:
+            True if this module has a test config, False otherwise.
+        """
+        # Check if test_config in module-info is set.
+        for test_config in mod_info.get(constants.MODULE_TEST_CONFIG, []):
+            if os.path.isfile(os.path.join(self.root_dir, test_config)):
+                return True
+        # Check for AndroidTest.xml at the module path.
+        for path in mod_info.get(constants.MODULE_PATH, []):
+            if os.path.isfile(os.path.join(self.root_dir, path,
+                                           constants.MODULE_CONFIG)):
+                return True
+        # Check if the module has an auto-generated config.
+        return self.is_auto_gen_test_config(mod_info.get(constants.MODULE_NAME))
+
+    def get_robolectric_test_name(self, module_name):
+        """Returns runnable robolectric module name.
+
+        There are at least 2 modules in every robolectric module path, return
+        the module that we can run as a build target.
+
+        Arg:
+            module_name: String of module.
+
+        Returns:
+            String of module that is the runnable robolectric module, None if
+            none could be found.
+        """
+        module_name_info = self.name_to_module_info.get(module_name)
+        if not module_name_info:
+            return None
+        module_paths = module_name_info.get(constants.MODULE_PATH, [])
+        if module_paths:
+            for mod in self.get_module_names(module_paths[0]):
+                mod_info = self.get_module_info(mod)
+                if self.is_robolectric_module(mod_info):
+                    return mod
+        return None
+
+    def is_robolectric_test(self, module_name):
+        """Check if module is a robolectric test.
+
+        A module can be a robolectric test if the specified module has their
+        class set as ROBOLECTRIC (or shares their path with a module that does).
+
+        Args:
+            module_name: String of module to check.
+
+        Returns:
+            True if the module is a robolectric module, else False.
+        """
+        # Check 1, module class is ROBOLECTRIC
+        mod_info = self.get_module_info(module_name)
+        if self.is_robolectric_module(mod_info):
+            return True
+        # Check 2, shared modules in the path have class ROBOLECTRIC_CLASS.
+        if self.get_robolectric_test_name(module_name):
+            return True
+        return False
+
+    def is_auto_gen_test_config(self, module_name):
+        """Check if the test config file will be generated automatically.
+
+        Args:
+            module_name: A string of the module name.
+
+        Returns:
+            True if the test config file will be generated automatically.
+        """
+        if self.is_module(module_name):
+            mod_info = self.name_to_module_info.get(module_name)
+            auto_test_config = mod_info.get('auto_test_config', [])
+            return auto_test_config and auto_test_config[0]
+        return False
+
+    def is_robolectric_module(self, mod_info):
+        """Check if a module is a robolectric module.
+
+        Args:
+            mod_info: ModuleInfo to check.
+
+        Returns:
+            True if module is a robolectric module, False otherwise.
+        """
+        if mod_info:
+            return (mod_info.get(constants.MODULE_CLASS, [None])[0] ==
+                    constants.MODULE_CLASS_ROBOLECTRIC)
+        return False
+
+    def is_native_test(self, module_name):
+        """Check if the input module is a native test.
+
+        Args:
+            module_name: A string of the module name.
+
+        Returns:
+            True if the test is a native test, False otherwise.
+        """
+        mod_info = self.get_module_info(module_name)
+        return constants.MODULE_CLASS_NATIVE_TESTS in mod_info.get(
+            constants.MODULE_CLASS, [])
diff --git a/atest/module_info_unittest.py b/atest/module_info_unittest.py
new file mode 100755
index 0000000..2570ab2
--- /dev/null
+++ b/atest/module_info_unittest.py
@@ -0,0 +1,287 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for module_info."""
+
+import os
+import unittest
+import mock
+
+import constants
+import module_info
+import unittest_constants as uc
+
+JSON_FILE_PATH = os.path.join(uc.TEST_DATA_DIR, uc.JSON_FILE)
+EXPECTED_MOD_TARGET = 'tradefed'
+EXPECTED_MOD_TARGET_PATH = ['tf/core']
+UNEXPECTED_MOD_TARGET = 'this_should_not_be_in_module-info.json'
+MOD_NO_PATH = 'module-no-path'
+PATH_TO_MULT_MODULES = 'shared/path/to/be/used'
+MULT_MOODULES_WITH_SHARED_PATH = ['module2', 'module1']
+PATH_TO_MULT_MODULES_WITH_MULTI_ARCH = 'shared/path/to/be/used2'
+TESTABLE_MODULES_WITH_SHARED_PATH = ['multiarch1', 'multiarch2', 'multiarch3', 'multiarch3_32']
+
+ROBO_MOD_PATH = ['/shared/robo/path']
+NON_RUN_ROBO_MOD_NAME = 'robo_mod'
+RUN_ROBO_MOD_NAME = 'run_robo_mod'
+NON_RUN_ROBO_MOD = {constants.MODULE_NAME: NON_RUN_ROBO_MOD_NAME,
+                    constants.MODULE_PATH: ROBO_MOD_PATH,
+                    constants.MODULE_CLASS: ['random_class']}
+RUN_ROBO_MOD = {constants.MODULE_NAME: RUN_ROBO_MOD_NAME,
+                constants.MODULE_PATH: ROBO_MOD_PATH,
+                constants.MODULE_CLASS: [constants.MODULE_CLASS_ROBOLECTRIC]}
+MOD_PATH_INFO_DICT = {ROBO_MOD_PATH[0]: [RUN_ROBO_MOD, NON_RUN_ROBO_MOD]}
+MOD_NAME_INFO_DICT = {
+    RUN_ROBO_MOD_NAME: RUN_ROBO_MOD,
+    NON_RUN_ROBO_MOD_NAME: NON_RUN_ROBO_MOD}
+MOD_NAME1 = 'mod1'
+MOD_NAME2 = 'mod2'
+MOD_NAME3 = 'mod3'
+MOD_NAME4 = 'mod4'
+MOD_INFO_DICT = {}
+MODULE_INFO = {constants.MODULE_NAME: 'random_name',
+               constants.MODULE_PATH: 'a/b/c/path',
+               constants.MODULE_CLASS: ['random_class']}
+NAME_TO_MODULE_INFO = {'random_name' : MODULE_INFO}
+
+#pylint: disable=protected-access
+class ModuleInfoUnittests(unittest.TestCase):
+    """Unit tests for module_info.py"""
+
+    @mock.patch('json.load', return_value={})
+    @mock.patch('__builtin__.open', new_callable=mock.mock_open)
+    @mock.patch('os.path.isfile', return_value=True)
+    def test_load_mode_info_file_out_dir_handling(self, _isfile, _open, _json):
+        """Test _load_module_info_file out dir handling."""
+        # Test out default out dir is used.
+        build_top = '/path/to/top'
+        default_out_dir = os.path.join(build_top, 'out/dir/here')
+        os_environ_mock = {'ANDROID_PRODUCT_OUT': default_out_dir,
+                           constants.ANDROID_BUILD_TOP: build_top}
+        default_out_dir_mod_targ = 'out/dir/here/module-info.json'
+        # Make sure module_info_target is what we think it is.
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            mod_info = module_info.ModuleInfo()
+            self.assertEqual(default_out_dir_mod_targ,
+                             mod_info.module_info_target)
+
+        # Test out custom out dir is used (OUT_DIR=dir2).
+        custom_out_dir = os.path.join(build_top, 'out2/dir/here')
+        os_environ_mock = {'ANDROID_PRODUCT_OUT': custom_out_dir,
+                           constants.ANDROID_BUILD_TOP: build_top}
+        custom_out_dir_mod_targ = 'out2/dir/here/module-info.json'
+        # Make sure module_info_target is what we think it is.
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            mod_info = module_info.ModuleInfo()
+            self.assertEqual(custom_out_dir_mod_targ,
+                             mod_info.module_info_target)
+
+        # Test out custom abs out dir is used (OUT_DIR=/tmp/out/dir2).
+        abs_custom_out_dir = '/tmp/out/dir'
+        os_environ_mock = {'ANDROID_PRODUCT_OUT': abs_custom_out_dir,
+                           constants.ANDROID_BUILD_TOP: build_top}
+        custom_abs_out_dir_mod_targ = '/tmp/out/dir/module-info.json'
+        # Make sure module_info_target is what we think it is.
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            mod_info = module_info.ModuleInfo()
+            self.assertEqual(custom_abs_out_dir_mod_targ,
+                             mod_info.module_info_target)
+
+    @mock.patch.object(module_info.ModuleInfo, '_load_module_info_file',)
+    def test_get_path_to_module_info(self, mock_load_module):
+        """Test that we correctly create the path to module info dict."""
+        mod_one = 'mod1'
+        mod_two = 'mod2'
+        mod_path_one = '/path/to/mod1'
+        mod_path_two = '/path/to/mod2'
+        mod_info_dict = {mod_one: {constants.MODULE_PATH: [mod_path_one],
+                                   constants.MODULE_NAME: mod_one},
+                         mod_two: {constants.MODULE_PATH: [mod_path_two],
+                                   constants.MODULE_NAME: mod_two}}
+        mock_load_module.return_value = ('mod_target', mod_info_dict)
+        path_to_mod_info = {mod_path_one: [{constants.MODULE_NAME: mod_one,
+                                            constants.MODULE_PATH: [mod_path_one]}],
+                            mod_path_two: [{constants.MODULE_NAME: mod_two,
+                                            constants.MODULE_PATH: [mod_path_two]}]}
+        mod_info = module_info.ModuleInfo()
+        self.assertDictEqual(path_to_mod_info,
+                             mod_info._get_path_to_module_info(mod_info_dict))
+
+    def test_is_module(self):
+        """Test that we get the module when it's properly loaded."""
+        # Load up the test json file and check that module is in it
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        self.assertTrue(mod_info.is_module(EXPECTED_MOD_TARGET))
+        self.assertFalse(mod_info.is_module(UNEXPECTED_MOD_TARGET))
+
+    def test_get_path(self):
+        """Test that we get the module path when it's properly loaded."""
+        # Load up the test json file and check that module is in it
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        self.assertEqual(mod_info.get_paths(EXPECTED_MOD_TARGET),
+                         EXPECTED_MOD_TARGET_PATH)
+        self.assertEqual(mod_info.get_paths(MOD_NO_PATH), [])
+
+    def test_get_module_names(self):
+        """test that we get the module name properly."""
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        self.assertEqual(mod_info.get_module_names(EXPECTED_MOD_TARGET_PATH[0]),
+                         [EXPECTED_MOD_TARGET])
+        self.assertEqual(mod_info.get_module_names(PATH_TO_MULT_MODULES),
+                         MULT_MOODULES_WITH_SHARED_PATH)
+
+    def test_path_to_mod_info(self):
+        """test that we get the module name properly."""
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        module_list = []
+        for path_to_mod_info in mod_info.path_to_module_info[PATH_TO_MULT_MODULES_WITH_MULTI_ARCH]:
+            module_list.append(path_to_mod_info.get(constants.MODULE_NAME))
+        module_list.sort()
+        TESTABLE_MODULES_WITH_SHARED_PATH.sort()
+        self.assertEqual(module_list, TESTABLE_MODULES_WITH_SHARED_PATH)
+
+    def test_is_suite_in_compatibility_suites(self):
+        """Test is_suite_in_compatibility_suites."""
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        info = {'compatibility_suites': []}
+        self.assertFalse(mod_info.is_suite_in_compatibility_suites("cts", info))
+        info2 = {'compatibility_suites': ["cts"]}
+        self.assertTrue(mod_info.is_suite_in_compatibility_suites("cts", info2))
+        self.assertFalse(mod_info.is_suite_in_compatibility_suites("vts", info2))
+        info3 = {'compatibility_suites': ["cts", "vts"]}
+        self.assertTrue(mod_info.is_suite_in_compatibility_suites("cts", info3))
+        self.assertTrue(mod_info.is_suite_in_compatibility_suites("vts", info3))
+        self.assertFalse(mod_info.is_suite_in_compatibility_suites("ats", info3))
+
+    @mock.patch.object(module_info.ModuleInfo, 'is_testable_module')
+    @mock.patch.object(module_info.ModuleInfo, 'is_suite_in_compatibility_suites')
+    def test_get_testable_modules(self, mock_is_suite_exist, mock_is_testable):
+        """Test get_testable_modules."""
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        mock_is_testable.return_value = False
+        self.assertEqual(mod_info.get_testable_modules(), set())
+        mod_info.name_to_module_info = NAME_TO_MODULE_INFO
+        mock_is_testable.return_value = True
+        mock_is_suite_exist.return_value = True
+        self.assertEqual(1, len(mod_info.get_testable_modules('test_suite')))
+        mock_is_suite_exist.return_value = False
+        self.assertEqual(0, len(mod_info.get_testable_modules('test_suite')))
+        self.assertEqual(1, len(mod_info.get_testable_modules()))
+
+    @mock.patch.object(module_info.ModuleInfo, 'has_test_config')
+    @mock.patch.object(module_info.ModuleInfo, 'is_robolectric_test')
+    def test_is_testable_module(self, mock_is_robo_test, mock_has_test_config):
+        """Test is_testable_module."""
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        mock_is_robo_test.return_value = False
+        mock_has_test_config.return_value = True
+        installed_module_info = {constants.MODULE_INSTALLED:
+                                 uc.DEFAULT_INSTALL_PATH}
+        non_installed_module_info = {constants.MODULE_NAME: 'rand_name'}
+        # Empty mod_info or a non-installed module.
+        self.assertFalse(mod_info.is_testable_module(non_installed_module_info))
+        self.assertFalse(mod_info.is_testable_module({}))
+        # Testable Module or is a robo module for non-installed module.
+        self.assertTrue(mod_info.is_testable_module(installed_module_info))
+        mock_has_test_config.return_value = False
+        self.assertFalse(mod_info.is_testable_module(installed_module_info))
+        mock_is_robo_test.return_value = True
+        self.assertTrue(mod_info.is_testable_module(non_installed_module_info))
+
+    @mock.patch.object(module_info.ModuleInfo, 'is_auto_gen_test_config')
+    def test_has_test_config(self, mock_is_auto_gen):
+        """Test has_test_config."""
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        info = {constants.MODULE_PATH:[uc.TEST_DATA_DIR]}
+        mock_is_auto_gen.return_value = True
+        # Validate we see the config when it's auto-generated.
+        self.assertTrue(mod_info.has_test_config(info))
+        self.assertTrue(mod_info.has_test_config({}))
+        # Validate when actual config exists and there's no auto-generated config.
+        mock_is_auto_gen.return_value = False
+        self.assertTrue(mod_info.has_test_config(info))
+        self.assertFalse(mod_info.has_test_config({}))
+        # Validate the case mod_info MODULE_TEST_CONFIG be set
+        info2 = {constants.MODULE_PATH:[uc.TEST_CONFIG_DATA_DIR],
+                 constants.MODULE_TEST_CONFIG:[os.path.join(uc.TEST_CONFIG_DATA_DIR, "a.xml")]}
+        self.assertTrue(mod_info.has_test_config(info2))
+
+    @mock.patch.object(module_info.ModuleInfo, 'get_module_names')
+    def test_get_robolectric_test_name(self, mock_get_module_names):
+        """Test get_robolectric_test_name."""
+        # Happy path testing, make sure we get the run robo target.
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        mod_info.name_to_module_info = MOD_NAME_INFO_DICT
+        mod_info.path_to_module_info = MOD_PATH_INFO_DICT
+        mock_get_module_names.return_value = [RUN_ROBO_MOD_NAME, NON_RUN_ROBO_MOD_NAME]
+        self.assertEqual(mod_info.get_robolectric_test_name(
+            NON_RUN_ROBO_MOD_NAME), RUN_ROBO_MOD_NAME)
+        # Let's also make sure we don't return anything when we're not supposed
+        # to.
+        mock_get_module_names.return_value = [NON_RUN_ROBO_MOD_NAME]
+        self.assertEqual(mod_info.get_robolectric_test_name(
+            NON_RUN_ROBO_MOD_NAME), None)
+
+    @mock.patch.object(module_info.ModuleInfo, 'get_module_info')
+    @mock.patch.object(module_info.ModuleInfo, 'get_module_names')
+    def test_is_robolectric_test(self, mock_get_module_names, mock_get_module_info):
+        """Test is_robolectric_test."""
+        # Happy path testing, make sure we get the run robo target.
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        mod_info.name_to_module_info = MOD_NAME_INFO_DICT
+        mod_info.path_to_module_info = MOD_PATH_INFO_DICT
+        mock_get_module_names.return_value = [RUN_ROBO_MOD_NAME, NON_RUN_ROBO_MOD_NAME]
+        mock_get_module_info.return_value = RUN_ROBO_MOD
+        # Test on a run robo module.
+        self.assertTrue(mod_info.is_robolectric_test(RUN_ROBO_MOD_NAME))
+        # Test on a non-run robo module but shares with a run robo module.
+        self.assertTrue(mod_info.is_robolectric_test(NON_RUN_ROBO_MOD_NAME))
+        # Make sure we don't find robo tests where they don't exist.
+        mock_get_module_info.return_value = None
+        self.assertFalse(mod_info.is_robolectric_test('rand_mod'))
+
+    @mock.patch.object(module_info.ModuleInfo, 'is_module')
+    def test_is_auto_gen_test_config(self, mock_is_module):
+        """Test is_auto_gen_test_config correctly detects the module."""
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        mock_is_module.return_value = True
+        is_auto_test_config = {'auto_test_config': [True]}
+        is_not_auto_test_config = {'auto_test_config': [False]}
+        is_not_auto_test_config_again = {'auto_test_config': []}
+        MOD_INFO_DICT[MOD_NAME1] = is_auto_test_config
+        MOD_INFO_DICT[MOD_NAME2] = is_not_auto_test_config
+        MOD_INFO_DICT[MOD_NAME3] = is_not_auto_test_config_again
+        MOD_INFO_DICT[MOD_NAME4] = {}
+        mod_info.name_to_module_info = MOD_INFO_DICT
+        self.assertTrue(mod_info.is_auto_gen_test_config(MOD_NAME1))
+        self.assertFalse(mod_info.is_auto_gen_test_config(MOD_NAME2))
+        self.assertFalse(mod_info.is_auto_gen_test_config(MOD_NAME3))
+        self.assertFalse(mod_info.is_auto_gen_test_config(MOD_NAME4))
+
+    def test_is_robolectric_module(self):
+        """Test is_robolectric_module correctly detects the module."""
+        mod_info = module_info.ModuleInfo(module_file=JSON_FILE_PATH)
+        is_robolectric_module = {'class': ['ROBOLECTRIC']}
+        is_not_robolectric_module = {'class': ['OTHERS']}
+        MOD_INFO_DICT[MOD_NAME1] = is_robolectric_module
+        MOD_INFO_DICT[MOD_NAME2] = is_not_robolectric_module
+        mod_info.name_to_module_info = MOD_INFO_DICT
+        self.assertTrue(mod_info.is_robolectric_module(MOD_INFO_DICT[MOD_NAME1]))
+        self.assertFalse(mod_info.is_robolectric_module(MOD_INFO_DICT[MOD_NAME2]))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/proto/__init__.py b/atest/proto/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/proto/__init__.py
diff --git a/atest/proto/clientanalytics.proto b/atest/proto/clientanalytics.proto
new file mode 100644
index 0000000..e75bf78
--- /dev/null
+++ b/atest/proto/clientanalytics.proto
@@ -0,0 +1,22 @@
+syntax = "proto2";
+
+option java_package = "com.android.asuite.clearcut";
+
+message LogRequest {
+  optional ClientInfo client_info = 1;
+  optional int32 log_source = 2;
+  optional int64 request_time_ms = 4;
+  repeated LogEvent log_event = 3;
+}
+message ClientInfo {
+  optional int32 client_type = 1;
+}
+
+message LogResponse {
+  optional int64 next_request_wait_millis = 1 ;
+}
+
+message LogEvent {
+  optional int64 event_time_ms = 1 ;
+  optional bytes source_extension = 6;
+}
diff --git a/atest/proto/clientanalytics_pb2.py b/atest/proto/clientanalytics_pb2.py
new file mode 100644
index 0000000..b58dcc7
--- /dev/null
+++ b/atest/proto/clientanalytics_pb2.py
@@ -0,0 +1,217 @@
+# pylint: skip-file
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: proto/clientanalytics.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='proto/clientanalytics.proto',
+  package='',
+  syntax='proto2',
+  serialized_pb=_b('\n\x1bproto/clientanalytics.proto\"y\n\nLogRequest\x12 \n\x0b\x63lient_info\x18\x01 \x01(\x0b\x32\x0b.ClientInfo\x12\x12\n\nlog_source\x18\x02 \x01(\x05\x12\x17\n\x0frequest_time_ms\x18\x04 \x01(\x03\x12\x1c\n\tlog_event\x18\x03 \x03(\x0b\x32\t.LogEvent\"!\n\nClientInfo\x12\x13\n\x0b\x63lient_type\x18\x01 \x01(\x05\"/\n\x0bLogResponse\x12 \n\x18next_request_wait_millis\x18\x01 \x01(\x03\";\n\x08LogEvent\x12\x15\n\revent_time_ms\x18\x01 \x01(\x03\x12\x18\n\x10source_extension\x18\x06 \x01(\x0c')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_LOGREQUEST = _descriptor.Descriptor(
+  name='LogRequest',
+  full_name='LogRequest',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='client_info', full_name='LogRequest.client_info', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='log_source', full_name='LogRequest.log_source', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='request_time_ms', full_name='LogRequest.request_time_ms', index=2,
+      number=4, type=3, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='log_event', full_name='LogRequest.log_event', index=3,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=31,
+  serialized_end=152,
+)
+
+
+_CLIENTINFO = _descriptor.Descriptor(
+  name='ClientInfo',
+  full_name='ClientInfo',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='client_type', full_name='ClientInfo.client_type', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=154,
+  serialized_end=187,
+)
+
+
+_LOGRESPONSE = _descriptor.Descriptor(
+  name='LogResponse',
+  full_name='LogResponse',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='next_request_wait_millis', full_name='LogResponse.next_request_wait_millis', index=0,
+      number=1, type=3, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=189,
+  serialized_end=236,
+)
+
+
+_LOGEVENT = _descriptor.Descriptor(
+  name='LogEvent',
+  full_name='LogEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='event_time_ms', full_name='LogEvent.event_time_ms', index=0,
+      number=1, type=3, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='source_extension', full_name='LogEvent.source_extension', index=1,
+      number=6, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b(""),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=238,
+  serialized_end=297,
+)
+
+_LOGREQUEST.fields_by_name['client_info'].message_type = _CLIENTINFO
+_LOGREQUEST.fields_by_name['log_event'].message_type = _LOGEVENT
+DESCRIPTOR.message_types_by_name['LogRequest'] = _LOGREQUEST
+DESCRIPTOR.message_types_by_name['ClientInfo'] = _CLIENTINFO
+DESCRIPTOR.message_types_by_name['LogResponse'] = _LOGRESPONSE
+DESCRIPTOR.message_types_by_name['LogEvent'] = _LOGEVENT
+
+LogRequest = _reflection.GeneratedProtocolMessageType('LogRequest', (_message.Message,), dict(
+  DESCRIPTOR = _LOGREQUEST,
+  __module__ = 'proto.clientanalytics_pb2'
+  # @@protoc_insertion_point(class_scope:LogRequest)
+  ))
+_sym_db.RegisterMessage(LogRequest)
+
+ClientInfo = _reflection.GeneratedProtocolMessageType('ClientInfo', (_message.Message,), dict(
+  DESCRIPTOR = _CLIENTINFO,
+  __module__ = 'proto.clientanalytics_pb2'
+  # @@protoc_insertion_point(class_scope:ClientInfo)
+  ))
+_sym_db.RegisterMessage(ClientInfo)
+
+LogResponse = _reflection.GeneratedProtocolMessageType('LogResponse', (_message.Message,), dict(
+  DESCRIPTOR = _LOGRESPONSE,
+  __module__ = 'proto.clientanalytics_pb2'
+  # @@protoc_insertion_point(class_scope:LogResponse)
+  ))
+_sym_db.RegisterMessage(LogResponse)
+
+LogEvent = _reflection.GeneratedProtocolMessageType('LogEvent', (_message.Message,), dict(
+  DESCRIPTOR = _LOGEVENT,
+  __module__ = 'proto.clientanalytics_pb2'
+  # @@protoc_insertion_point(class_scope:LogEvent)
+  ))
+_sym_db.RegisterMessage(LogEvent)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/atest/proto/common.proto b/atest/proto/common.proto
new file mode 100644
index 0000000..49cc48c
--- /dev/null
+++ b/atest/proto/common.proto
@@ -0,0 +1,16 @@
+syntax = "proto2";
+
+option java_package = "com.android.asuite.clearcut";
+
+message Duration {
+  required int64 seconds = 1;
+  required int32 nanos = 2;
+}
+
+// ----------------
+// ENUM DEFINITIONS
+// ----------------
+enum UserType {
+  GOOGLE = 0;
+  EXTERNAL = 1;
+}
diff --git a/atest/proto/common_pb2.py b/atest/proto/common_pb2.py
new file mode 100644
index 0000000..5b7bd2e
--- /dev/null
+++ b/atest/proto/common_pb2.py
@@ -0,0 +1,104 @@
+# pylint: skip-file
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: proto/common.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf.internal import enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='proto/common.proto',
+  package='',
+  syntax='proto2',
+  serialized_pb=_b('\n\x12proto/common.proto\"*\n\x08\x44uration\x12\x0f\n\x07seconds\x18\x01 \x02(\x03\x12\r\n\x05nanos\x18\x02 \x02(\x05*$\n\x08UserType\x12\n\n\x06GOOGLE\x10\x00\x12\x0c\n\x08\x45XTERNAL\x10\x01')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+_USERTYPE = _descriptor.EnumDescriptor(
+  name='UserType',
+  full_name='UserType',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='GOOGLE', index=0, number=0,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='EXTERNAL', index=1, number=1,
+      options=None,
+      type=None),
+  ],
+  containing_type=None,
+  options=None,
+  serialized_start=66,
+  serialized_end=102,
+)
+_sym_db.RegisterEnumDescriptor(_USERTYPE)
+
+UserType = enum_type_wrapper.EnumTypeWrapper(_USERTYPE)
+GOOGLE = 0
+EXTERNAL = 1
+
+
+
+_DURATION = _descriptor.Descriptor(
+  name='Duration',
+  full_name='Duration',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='seconds', full_name='Duration.seconds', index=0,
+      number=1, type=3, cpp_type=2, label=2,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='nanos', full_name='Duration.nanos', index=1,
+      number=2, type=5, cpp_type=1, label=2,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=22,
+  serialized_end=64,
+)
+
+DESCRIPTOR.message_types_by_name['Duration'] = _DURATION
+DESCRIPTOR.enum_types_by_name['UserType'] = _USERTYPE
+
+Duration = _reflection.GeneratedProtocolMessageType('Duration', (_message.Message,), dict(
+  DESCRIPTOR = _DURATION,
+  __module__ = 'proto.common_pb2'
+  # @@protoc_insertion_point(class_scope:Duration)
+  ))
+_sym_db.RegisterMessage(Duration)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/atest/proto/external_user_log.proto b/atest/proto/external_user_log.proto
new file mode 100644
index 0000000..533ff0a
--- /dev/null
+++ b/atest/proto/external_user_log.proto
@@ -0,0 +1,70 @@
+syntax = "proto2";
+
+import "proto/common.proto";
+
+option java_package = "com.android.asuite.clearcut";
+
+// Proto used by Atest CLI Tool for External Non-PII Users
+message AtestLogEventExternal {
+
+  // ------------------------
+  // EVENT DEFINITIONS
+  // ------------------------
+  // Occurs immediately upon execution of atest
+  message AtestStartEvent {
+  }
+
+  // Occurs when atest exits for any reason
+  message AtestExitEvent {
+    optional Duration duration = 1;
+    optional int32 exit_code = 2;
+  }
+
+  // Occurs after a SINGLE test reference has been resolved to a test or
+  // not found
+  message FindTestFinishEvent {
+    optional Duration duration = 1;
+    optional bool success = 2;
+  }
+
+  // Occurs after the build finishes, either successfully or not.
+  message BuildFinishEvent {
+    optional Duration duration = 1;
+    optional bool success = 2;
+  }
+
+  // Occurs when a single test runner has completed
+  message RunnerFinishEvent {
+    optional Duration duration = 1;
+    optional bool success = 2;
+    optional string runner_name = 3;
+  }
+
+  // Occurs after all test runners and tests have finished
+  message RunTestsFinishEvent {
+    optional Duration duration = 1;
+  }
+
+  // Occurs after detection of catching bug by atest have finished
+  message LocalDetectEvent {
+    optional int32 detect_type = 1;
+    optional int32 result = 2;
+  }
+
+  // ------------------------
+  // FIELDS FOR ATESTLOGEVENT
+  // ------------------------
+  optional string user_key = 1;
+  optional string run_id = 2;
+  optional UserType user_type = 3;
+  optional string tool_name = 10;
+  oneof event {
+    AtestStartEvent atest_start_event = 4;
+    AtestExitEvent atest_exit_event = 5;
+    FindTestFinishEvent find_test_finish_event= 6;
+    BuildFinishEvent build_finish_event = 7;
+    RunnerFinishEvent runner_finish_event = 8;
+    RunTestsFinishEvent run_tests_finish_event = 9;
+    LocalDetectEvent local_detect_event = 11;
+  }
+}
diff --git a/atest/proto/external_user_log_pb2.py b/atest/proto/external_user_log_pb2.py
new file mode 100644
index 0000000..ba33fd4
--- /dev/null
+++ b/atest/proto/external_user_log_pb2.py
@@ -0,0 +1,487 @@
+# pylint: skip-file
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: proto/external_user_log.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from proto import common_pb2 as proto_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='proto/external_user_log.proto',
+  package='',
+  syntax='proto2',
+  serialized_pb=_b('\n\x1dproto/external_user_log.proto\x1a\x12proto/common.proto\"\x8f\x08\n\x15\x41testLogEventExternal\x12\x10\n\x08user_key\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x1c\n\tuser_type\x18\x03 \x01(\x0e\x32\t.UserType\x12\x11\n\ttool_name\x18\n \x01(\t\x12\x43\n\x11\x61test_start_event\x18\x04 \x01(\x0b\x32&.AtestLogEventExternal.AtestStartEventH\x00\x12\x41\n\x10\x61test_exit_event\x18\x05 \x01(\x0b\x32%.AtestLogEventExternal.AtestExitEventH\x00\x12L\n\x16\x66ind_test_finish_event\x18\x06 \x01(\x0b\x32*.AtestLogEventExternal.FindTestFinishEventH\x00\x12\x45\n\x12\x62uild_finish_event\x18\x07 \x01(\x0b\x32\'.AtestLogEventExternal.BuildFinishEventH\x00\x12G\n\x13runner_finish_event\x18\x08 \x01(\x0b\x32(.AtestLogEventExternal.RunnerFinishEventH\x00\x12L\n\x16run_tests_finish_event\x18\t \x01(\x0b\x32*.AtestLogEventExternal.RunTestsFinishEventH\x00\x12\x45\n\x12local_detect_event\x18\x0b \x01(\x0b\x32\'.AtestLogEventExternal.LocalDetectEventH\x00\x1a\x11\n\x0f\x41testStartEvent\x1a@\n\x0e\x41testExitEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x12\x11\n\texit_code\x18\x02 \x01(\x05\x1a\x43\n\x13\x46indTestFinishEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x12\x0f\n\x07success\x18\x02 \x01(\x08\x1a@\n\x10\x42uildFinishEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x12\x0f\n\x07success\x18\x02 \x01(\x08\x1aV\n\x11RunnerFinishEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x13\n\x0brunner_name\x18\x03 \x01(\t\x1a\x32\n\x13RunTestsFinishEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x1a\x37\n\x10LocalDetectEvent\x12\x13\n\x0b\x64\x65tect_type\x18\x01 \x01(\x05\x12\x0e\n\x06result\x18\x02 \x01(\x05\x42\x07\n\x05\x65vent')
+  ,
+  dependencies=[proto_dot_common__pb2.DESCRIPTOR,])
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_ATESTLOGEVENTEXTERNAL_ATESTSTARTEVENT = _descriptor.Descriptor(
+  name='AtestStartEvent',
+  full_name='AtestLogEventExternal.AtestStartEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=669,
+  serialized_end=686,
+)
+
+_ATESTLOGEVENTEXTERNAL_ATESTEXITEVENT = _descriptor.Descriptor(
+  name='AtestExitEvent',
+  full_name='AtestLogEventExternal.AtestExitEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventExternal.AtestExitEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='exit_code', full_name='AtestLogEventExternal.AtestExitEvent.exit_code', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=688,
+  serialized_end=752,
+)
+
+_ATESTLOGEVENTEXTERNAL_FINDTESTFINISHEVENT = _descriptor.Descriptor(
+  name='FindTestFinishEvent',
+  full_name='AtestLogEventExternal.FindTestFinishEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventExternal.FindTestFinishEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='success', full_name='AtestLogEventExternal.FindTestFinishEvent.success', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=754,
+  serialized_end=821,
+)
+
+_ATESTLOGEVENTEXTERNAL_BUILDFINISHEVENT = _descriptor.Descriptor(
+  name='BuildFinishEvent',
+  full_name='AtestLogEventExternal.BuildFinishEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventExternal.BuildFinishEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='success', full_name='AtestLogEventExternal.BuildFinishEvent.success', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=823,
+  serialized_end=887,
+)
+
+_ATESTLOGEVENTEXTERNAL_RUNNERFINISHEVENT = _descriptor.Descriptor(
+  name='RunnerFinishEvent',
+  full_name='AtestLogEventExternal.RunnerFinishEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventExternal.RunnerFinishEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='success', full_name='AtestLogEventExternal.RunnerFinishEvent.success', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='runner_name', full_name='AtestLogEventExternal.RunnerFinishEvent.runner_name', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=889,
+  serialized_end=975,
+)
+
+_ATESTLOGEVENTEXTERNAL_RUNTESTSFINISHEVENT = _descriptor.Descriptor(
+  name='RunTestsFinishEvent',
+  full_name='AtestLogEventExternal.RunTestsFinishEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventExternal.RunTestsFinishEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=977,
+  serialized_end=1027,
+)
+
+_ATESTLOGEVENTEXTERNAL_LOCALDETECTEVENT = _descriptor.Descriptor(
+  name='LocalDetectEvent',
+  full_name='AtestLogEventExternal.LocalDetectEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='detect_type', full_name='AtestLogEventExternal.LocalDetectEvent.detect_type', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='result', full_name='AtestLogEventExternal.LocalDetectEvent.result', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1029,
+  serialized_end=1084,
+)
+
+_ATESTLOGEVENTEXTERNAL = _descriptor.Descriptor(
+  name='AtestLogEventExternal',
+  full_name='AtestLogEventExternal',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='user_key', full_name='AtestLogEventExternal.user_key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='run_id', full_name='AtestLogEventExternal.run_id', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='user_type', full_name='AtestLogEventExternal.user_type', index=2,
+      number=3, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='tool_name', full_name='AtestLogEventExternal.tool_name', index=3,
+      number=10, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='atest_start_event', full_name='AtestLogEventExternal.atest_start_event', index=4,
+      number=4, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='atest_exit_event', full_name='AtestLogEventExternal.atest_exit_event', index=5,
+      number=5, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='find_test_finish_event', full_name='AtestLogEventExternal.find_test_finish_event', index=6,
+      number=6, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='build_finish_event', full_name='AtestLogEventExternal.build_finish_event', index=7,
+      number=7, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='runner_finish_event', full_name='AtestLogEventExternal.runner_finish_event', index=8,
+      number=8, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='run_tests_finish_event', full_name='AtestLogEventExternal.run_tests_finish_event', index=9,
+      number=9, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='local_detect_event', full_name='AtestLogEventExternal.local_detect_event', index=10,
+      number=11, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_ATESTLOGEVENTEXTERNAL_ATESTSTARTEVENT, _ATESTLOGEVENTEXTERNAL_ATESTEXITEVENT, _ATESTLOGEVENTEXTERNAL_FINDTESTFINISHEVENT, _ATESTLOGEVENTEXTERNAL_BUILDFINISHEVENT, _ATESTLOGEVENTEXTERNAL_RUNNERFINISHEVENT, _ATESTLOGEVENTEXTERNAL_RUNTESTSFINISHEVENT, _ATESTLOGEVENTEXTERNAL_LOCALDETECTEVENT, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+    _descriptor.OneofDescriptor(
+      name='event', full_name='AtestLogEventExternal.event',
+      index=0, containing_type=None, fields=[]),
+  ],
+  serialized_start=54,
+  serialized_end=1093,
+)
+
+_ATESTLOGEVENTEXTERNAL_ATESTSTARTEVENT.containing_type = _ATESTLOGEVENTEXTERNAL
+_ATESTLOGEVENTEXTERNAL_ATESTEXITEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTEXTERNAL_ATESTEXITEVENT.containing_type = _ATESTLOGEVENTEXTERNAL
+_ATESTLOGEVENTEXTERNAL_FINDTESTFINISHEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTEXTERNAL_FINDTESTFINISHEVENT.containing_type = _ATESTLOGEVENTEXTERNAL
+_ATESTLOGEVENTEXTERNAL_BUILDFINISHEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTEXTERNAL_BUILDFINISHEVENT.containing_type = _ATESTLOGEVENTEXTERNAL
+_ATESTLOGEVENTEXTERNAL_RUNNERFINISHEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTEXTERNAL_RUNNERFINISHEVENT.containing_type = _ATESTLOGEVENTEXTERNAL
+_ATESTLOGEVENTEXTERNAL_RUNTESTSFINISHEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTEXTERNAL_RUNTESTSFINISHEVENT.containing_type = _ATESTLOGEVENTEXTERNAL
+_ATESTLOGEVENTEXTERNAL_LOCALDETECTEVENT.containing_type = _ATESTLOGEVENTEXTERNAL
+_ATESTLOGEVENTEXTERNAL.fields_by_name['user_type'].enum_type = proto_dot_common__pb2._USERTYPE
+_ATESTLOGEVENTEXTERNAL.fields_by_name['atest_start_event'].message_type = _ATESTLOGEVENTEXTERNAL_ATESTSTARTEVENT
+_ATESTLOGEVENTEXTERNAL.fields_by_name['atest_exit_event'].message_type = _ATESTLOGEVENTEXTERNAL_ATESTEXITEVENT
+_ATESTLOGEVENTEXTERNAL.fields_by_name['find_test_finish_event'].message_type = _ATESTLOGEVENTEXTERNAL_FINDTESTFINISHEVENT
+_ATESTLOGEVENTEXTERNAL.fields_by_name['build_finish_event'].message_type = _ATESTLOGEVENTEXTERNAL_BUILDFINISHEVENT
+_ATESTLOGEVENTEXTERNAL.fields_by_name['runner_finish_event'].message_type = _ATESTLOGEVENTEXTERNAL_RUNNERFINISHEVENT
+_ATESTLOGEVENTEXTERNAL.fields_by_name['run_tests_finish_event'].message_type = _ATESTLOGEVENTEXTERNAL_RUNTESTSFINISHEVENT
+_ATESTLOGEVENTEXTERNAL.fields_by_name['local_detect_event'].message_type = _ATESTLOGEVENTEXTERNAL_LOCALDETECTEVENT
+_ATESTLOGEVENTEXTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTEXTERNAL.fields_by_name['atest_start_event'])
+_ATESTLOGEVENTEXTERNAL.fields_by_name['atest_start_event'].containing_oneof = _ATESTLOGEVENTEXTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTEXTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTEXTERNAL.fields_by_name['atest_exit_event'])
+_ATESTLOGEVENTEXTERNAL.fields_by_name['atest_exit_event'].containing_oneof = _ATESTLOGEVENTEXTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTEXTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTEXTERNAL.fields_by_name['find_test_finish_event'])
+_ATESTLOGEVENTEXTERNAL.fields_by_name['find_test_finish_event'].containing_oneof = _ATESTLOGEVENTEXTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTEXTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTEXTERNAL.fields_by_name['build_finish_event'])
+_ATESTLOGEVENTEXTERNAL.fields_by_name['build_finish_event'].containing_oneof = _ATESTLOGEVENTEXTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTEXTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTEXTERNAL.fields_by_name['runner_finish_event'])
+_ATESTLOGEVENTEXTERNAL.fields_by_name['runner_finish_event'].containing_oneof = _ATESTLOGEVENTEXTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTEXTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTEXTERNAL.fields_by_name['run_tests_finish_event'])
+_ATESTLOGEVENTEXTERNAL.fields_by_name['run_tests_finish_event'].containing_oneof = _ATESTLOGEVENTEXTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTEXTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTEXTERNAL.fields_by_name['local_detect_event'])
+_ATESTLOGEVENTEXTERNAL.fields_by_name['local_detect_event'].containing_oneof = _ATESTLOGEVENTEXTERNAL.oneofs_by_name['event']
+DESCRIPTOR.message_types_by_name['AtestLogEventExternal'] = _ATESTLOGEVENTEXTERNAL
+
+AtestLogEventExternal = _reflection.GeneratedProtocolMessageType('AtestLogEventExternal', (_message.Message,), dict(
+
+  AtestStartEvent = _reflection.GeneratedProtocolMessageType('AtestStartEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTEXTERNAL_ATESTSTARTEVENT,
+    __module__ = 'proto.external_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventExternal.AtestStartEvent)
+    ))
+  ,
+
+  AtestExitEvent = _reflection.GeneratedProtocolMessageType('AtestExitEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTEXTERNAL_ATESTEXITEVENT,
+    __module__ = 'proto.external_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventExternal.AtestExitEvent)
+    ))
+  ,
+
+  FindTestFinishEvent = _reflection.GeneratedProtocolMessageType('FindTestFinishEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTEXTERNAL_FINDTESTFINISHEVENT,
+    __module__ = 'proto.external_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventExternal.FindTestFinishEvent)
+    ))
+  ,
+
+  BuildFinishEvent = _reflection.GeneratedProtocolMessageType('BuildFinishEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTEXTERNAL_BUILDFINISHEVENT,
+    __module__ = 'proto.external_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventExternal.BuildFinishEvent)
+    ))
+  ,
+
+  RunnerFinishEvent = _reflection.GeneratedProtocolMessageType('RunnerFinishEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTEXTERNAL_RUNNERFINISHEVENT,
+    __module__ = 'proto.external_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventExternal.RunnerFinishEvent)
+    ))
+  ,
+
+  RunTestsFinishEvent = _reflection.GeneratedProtocolMessageType('RunTestsFinishEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTEXTERNAL_RUNTESTSFINISHEVENT,
+    __module__ = 'proto.external_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventExternal.RunTestsFinishEvent)
+    ))
+  ,
+
+  LocalDetectEvent = _reflection.GeneratedProtocolMessageType('LocalDetectEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTEXTERNAL_LOCALDETECTEVENT,
+    __module__ = 'proto.external_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventExternal.LocalDetectEvent)
+    ))
+  ,
+  DESCRIPTOR = _ATESTLOGEVENTEXTERNAL,
+  __module__ = 'proto.external_user_log_pb2'
+  # @@protoc_insertion_point(class_scope:AtestLogEventExternal)
+  ))
+_sym_db.RegisterMessage(AtestLogEventExternal)
+_sym_db.RegisterMessage(AtestLogEventExternal.AtestStartEvent)
+_sym_db.RegisterMessage(AtestLogEventExternal.AtestExitEvent)
+_sym_db.RegisterMessage(AtestLogEventExternal.FindTestFinishEvent)
+_sym_db.RegisterMessage(AtestLogEventExternal.BuildFinishEvent)
+_sym_db.RegisterMessage(AtestLogEventExternal.RunnerFinishEvent)
+_sym_db.RegisterMessage(AtestLogEventExternal.RunTestsFinishEvent)
+_sym_db.RegisterMessage(AtestLogEventExternal.LocalDetectEvent)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/atest/proto/internal_user_log.proto b/atest/proto/internal_user_log.proto
new file mode 100644
index 0000000..05d4dee
--- /dev/null
+++ b/atest/proto/internal_user_log.proto
@@ -0,0 +1,86 @@
+syntax = "proto2";
+
+import "proto/common.proto";
+
+option java_package = "com.android.asuite.clearcut";
+
+// Proto used by Atest CLI Tool for internal Users
+message AtestLogEventInternal {
+
+  // ------------------------
+  // EVENT DEFINITIONS
+  // ------------------------
+  // Occurs immediately upon execution of atest
+  message AtestStartEvent {
+    optional string command_line = 1;
+    repeated string test_references = 2;
+    optional string cwd = 3;
+    optional string os = 4;
+  }
+
+  // Occurs when atest exits for any reason
+  message AtestExitEvent {
+    optional Duration duration = 1;
+    optional int32 exit_code = 2;
+    optional string stacktrace = 3;
+    optional string logs = 4;
+  }
+
+  // Occurs after a SINGLE test reference has been resolved to a test or
+  // not found
+  message FindTestFinishEvent {
+    optional Duration duration = 1;
+    optional bool success = 2;
+    optional string test_reference = 3;
+    repeated string test_finders = 4;
+    optional string test_info = 5;
+  }
+
+  // Occurs after the build finishes, either successfully or not.
+  message BuildFinishEvent {
+    optional Duration duration = 1;
+    optional bool success = 2;
+    repeated string targets = 3;
+  }
+
+  // Occurs when a single test runner has completed
+  message RunnerFinishEvent {
+    optional Duration duration = 1;
+    optional bool success = 2;
+    optional string runner_name = 3;
+    message Test {
+      optional string name = 1;
+      optional int32 result = 2;
+      optional string stacktrace = 3;
+    }
+    repeated Test test = 4;
+  }
+
+  // Occurs after all test runners and tests have finished
+  message RunTestsFinishEvent {
+    optional Duration duration = 1;
+  }
+
+  // Occurs after detection of catching bug by atest have finished
+  message LocalDetectEvent {
+    optional int32 detect_type = 1;
+    optional int32 result = 2;
+  }
+
+  // ------------------------
+  // FIELDS FOR ATESTLOGEVENT
+  // ------------------------
+  optional string user_key = 1;
+  optional string run_id = 2;
+  optional UserType user_type = 3;
+  optional string tool_name = 10;
+  oneof event {
+    AtestStartEvent atest_start_event = 4;
+    AtestExitEvent atest_exit_event = 5;
+    FindTestFinishEvent find_test_finish_event= 6;
+    BuildFinishEvent build_finish_event = 7;
+    RunnerFinishEvent runner_finish_event = 8;
+    RunTestsFinishEvent run_tests_finish_event = 9;
+    LocalDetectEvent local_detect_event = 11;
+  }
+}
diff --git a/atest/proto/internal_user_log_pb2.py b/atest/proto/internal_user_log_pb2.py
new file mode 100644
index 0000000..e8585dc
--- /dev/null
+++ b/atest/proto/internal_user_log_pb2.py
@@ -0,0 +1,618 @@
+# pylint: skip-file
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: proto/internal_user_log.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from proto import common_pb2 as proto_dot_common__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='proto/internal_user_log.proto',
+  package='',
+  syntax='proto2',
+  serialized_pb=_b('\n\x1dproto/internal_user_log.proto\x1a\x12proto/common.proto\"\xc4\n\n\x15\x41testLogEventInternal\x12\x10\n\x08user_key\x18\x01 \x01(\t\x12\x0e\n\x06run_id\x18\x02 \x01(\t\x12\x1c\n\tuser_type\x18\x03 \x01(\x0e\x32\t.UserType\x12\x11\n\ttool_name\x18\n \x01(\t\x12\x43\n\x11\x61test_start_event\x18\x04 \x01(\x0b\x32&.AtestLogEventInternal.AtestStartEventH\x00\x12\x41\n\x10\x61test_exit_event\x18\x05 \x01(\x0b\x32%.AtestLogEventInternal.AtestExitEventH\x00\x12L\n\x16\x66ind_test_finish_event\x18\x06 \x01(\x0b\x32*.AtestLogEventInternal.FindTestFinishEventH\x00\x12\x45\n\x12\x62uild_finish_event\x18\x07 \x01(\x0b\x32\'.AtestLogEventInternal.BuildFinishEventH\x00\x12G\n\x13runner_finish_event\x18\x08 \x01(\x0b\x32(.AtestLogEventInternal.RunnerFinishEventH\x00\x12L\n\x16run_tests_finish_event\x18\t \x01(\x0b\x32*.AtestLogEventInternal.RunTestsFinishEventH\x00\x12\x45\n\x12local_detect_event\x18\x0b \x01(\x0b\x32\'.AtestLogEventInternal.LocalDetectEventH\x00\x1aY\n\x0f\x41testStartEvent\x12\x14\n\x0c\x63ommand_line\x18\x01 \x01(\t\x12\x17\n\x0ftest_references\x18\x02 \x03(\t\x12\x0b\n\x03\x63wd\x18\x03 \x01(\t\x12\n\n\x02os\x18\x04 \x01(\t\x1a\x62\n\x0e\x41testExitEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x12\x11\n\texit_code\x18\x02 \x01(\x05\x12\x12\n\nstacktrace\x18\x03 \x01(\t\x12\x0c\n\x04logs\x18\x04 \x01(\t\x1a\x84\x01\n\x13\x46indTestFinishEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x16\n\x0etest_reference\x18\x03 \x01(\t\x12\x14\n\x0ctest_finders\x18\x04 \x03(\t\x12\x11\n\ttest_info\x18\x05 \x01(\t\x1aQ\n\x10\x42uildFinishEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x0f\n\x07targets\x18\x03 \x03(\t\x1a\xcd\x01\n\x11RunnerFinishEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x13\n\x0brunner_name\x18\x03 \x01(\t\x12;\n\x04test\x18\x04 \x03(\x0b\x32-.AtestLogEventInternal.RunnerFinishEvent.Test\x1a\x38\n\x04Test\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\x05\x12\x12\n\nstacktrace\x18\x03 \x01(\t\x1a\x32\n\x13RunTestsFinishEvent\x12\x1b\n\x08\x64uration\x18\x01 \x01(\x0b\x32\t.Duration\x1a\x37\n\x10LocalDetectEvent\x12\x13\n\x0b\x64\x65tect_type\x18\x01 \x01(\x05\x12\x0e\n\x06result\x18\x02 \x01(\x05\x42\x07\n\x05\x65vent')
+  ,
+  dependencies=[proto_dot_common__pb2.DESCRIPTOR,])
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_ATESTLOGEVENTINTERNAL_ATESTSTARTEVENT = _descriptor.Descriptor(
+  name='AtestStartEvent',
+  full_name='AtestLogEventInternal.AtestStartEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='command_line', full_name='AtestLogEventInternal.AtestStartEvent.command_line', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='test_references', full_name='AtestLogEventInternal.AtestStartEvent.test_references', index=1,
+      number=2, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='cwd', full_name='AtestLogEventInternal.AtestStartEvent.cwd', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='os', full_name='AtestLogEventInternal.AtestStartEvent.os', index=3,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=669,
+  serialized_end=758,
+)
+
+_ATESTLOGEVENTINTERNAL_ATESTEXITEVENT = _descriptor.Descriptor(
+  name='AtestExitEvent',
+  full_name='AtestLogEventInternal.AtestExitEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventInternal.AtestExitEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='exit_code', full_name='AtestLogEventInternal.AtestExitEvent.exit_code', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='stacktrace', full_name='AtestLogEventInternal.AtestExitEvent.stacktrace', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='logs', full_name='AtestLogEventInternal.AtestExitEvent.logs', index=3,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=760,
+  serialized_end=858,
+)
+
+_ATESTLOGEVENTINTERNAL_FINDTESTFINISHEVENT = _descriptor.Descriptor(
+  name='FindTestFinishEvent',
+  full_name='AtestLogEventInternal.FindTestFinishEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventInternal.FindTestFinishEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='success', full_name='AtestLogEventInternal.FindTestFinishEvent.success', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='test_reference', full_name='AtestLogEventInternal.FindTestFinishEvent.test_reference', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='test_finders', full_name='AtestLogEventInternal.FindTestFinishEvent.test_finders', index=3,
+      number=4, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='test_info', full_name='AtestLogEventInternal.FindTestFinishEvent.test_info', index=4,
+      number=5, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=861,
+  serialized_end=993,
+)
+
+_ATESTLOGEVENTINTERNAL_BUILDFINISHEVENT = _descriptor.Descriptor(
+  name='BuildFinishEvent',
+  full_name='AtestLogEventInternal.BuildFinishEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventInternal.BuildFinishEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='success', full_name='AtestLogEventInternal.BuildFinishEvent.success', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='targets', full_name='AtestLogEventInternal.BuildFinishEvent.targets', index=2,
+      number=3, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=995,
+  serialized_end=1076,
+)
+
+_ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT_TEST = _descriptor.Descriptor(
+  name='Test',
+  full_name='AtestLogEventInternal.RunnerFinishEvent.Test',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='name', full_name='AtestLogEventInternal.RunnerFinishEvent.Test.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='result', full_name='AtestLogEventInternal.RunnerFinishEvent.Test.result', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='stacktrace', full_name='AtestLogEventInternal.RunnerFinishEvent.Test.stacktrace', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1228,
+  serialized_end=1284,
+)
+
+_ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT = _descriptor.Descriptor(
+  name='RunnerFinishEvent',
+  full_name='AtestLogEventInternal.RunnerFinishEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventInternal.RunnerFinishEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='success', full_name='AtestLogEventInternal.RunnerFinishEvent.success', index=1,
+      number=2, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='runner_name', full_name='AtestLogEventInternal.RunnerFinishEvent.runner_name', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='test', full_name='AtestLogEventInternal.RunnerFinishEvent.test', index=3,
+      number=4, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT_TEST, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1079,
+  serialized_end=1284,
+)
+
+_ATESTLOGEVENTINTERNAL_RUNTESTSFINISHEVENT = _descriptor.Descriptor(
+  name='RunTestsFinishEvent',
+  full_name='AtestLogEventInternal.RunTestsFinishEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='duration', full_name='AtestLogEventInternal.RunTestsFinishEvent.duration', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1286,
+  serialized_end=1336,
+)
+
+_ATESTLOGEVENTINTERNAL_LOCALDETECTEVENT = _descriptor.Descriptor(
+  name='LocalDetectEvent',
+  full_name='AtestLogEventInternal.LocalDetectEvent',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='detect_type', full_name='AtestLogEventInternal.LocalDetectEvent.detect_type', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='result', full_name='AtestLogEventInternal.LocalDetectEvent.result', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1338,
+  serialized_end=1393,
+)
+
+_ATESTLOGEVENTINTERNAL = _descriptor.Descriptor(
+  name='AtestLogEventInternal',
+  full_name='AtestLogEventInternal',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='user_key', full_name='AtestLogEventInternal.user_key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='run_id', full_name='AtestLogEventInternal.run_id', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='user_type', full_name='AtestLogEventInternal.user_type', index=2,
+      number=3, type=14, cpp_type=8, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='tool_name', full_name='AtestLogEventInternal.tool_name', index=3,
+      number=10, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='atest_start_event', full_name='AtestLogEventInternal.atest_start_event', index=4,
+      number=4, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='atest_exit_event', full_name='AtestLogEventInternal.atest_exit_event', index=5,
+      number=5, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='find_test_finish_event', full_name='AtestLogEventInternal.find_test_finish_event', index=6,
+      number=6, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='build_finish_event', full_name='AtestLogEventInternal.build_finish_event', index=7,
+      number=7, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='runner_finish_event', full_name='AtestLogEventInternal.runner_finish_event', index=8,
+      number=8, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='run_tests_finish_event', full_name='AtestLogEventInternal.run_tests_finish_event', index=9,
+      number=9, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='local_detect_event', full_name='AtestLogEventInternal.local_detect_event', index=10,
+      number=11, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_ATESTLOGEVENTINTERNAL_ATESTSTARTEVENT, _ATESTLOGEVENTINTERNAL_ATESTEXITEVENT, _ATESTLOGEVENTINTERNAL_FINDTESTFINISHEVENT, _ATESTLOGEVENTINTERNAL_BUILDFINISHEVENT, _ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT, _ATESTLOGEVENTINTERNAL_RUNTESTSFINISHEVENT, _ATESTLOGEVENTINTERNAL_LOCALDETECTEVENT, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+    _descriptor.OneofDescriptor(
+      name='event', full_name='AtestLogEventInternal.event',
+      index=0, containing_type=None, fields=[]),
+  ],
+  serialized_start=54,
+  serialized_end=1402,
+)
+
+_ATESTLOGEVENTINTERNAL_ATESTSTARTEVENT.containing_type = _ATESTLOGEVENTINTERNAL
+_ATESTLOGEVENTINTERNAL_ATESTEXITEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTINTERNAL_ATESTEXITEVENT.containing_type = _ATESTLOGEVENTINTERNAL
+_ATESTLOGEVENTINTERNAL_FINDTESTFINISHEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTINTERNAL_FINDTESTFINISHEVENT.containing_type = _ATESTLOGEVENTINTERNAL
+_ATESTLOGEVENTINTERNAL_BUILDFINISHEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTINTERNAL_BUILDFINISHEVENT.containing_type = _ATESTLOGEVENTINTERNAL
+_ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT_TEST.containing_type = _ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT
+_ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT.fields_by_name['test'].message_type = _ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT_TEST
+_ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT.containing_type = _ATESTLOGEVENTINTERNAL
+_ATESTLOGEVENTINTERNAL_RUNTESTSFINISHEVENT.fields_by_name['duration'].message_type = proto_dot_common__pb2._DURATION
+_ATESTLOGEVENTINTERNAL_RUNTESTSFINISHEVENT.containing_type = _ATESTLOGEVENTINTERNAL
+_ATESTLOGEVENTINTERNAL_LOCALDETECTEVENT.containing_type = _ATESTLOGEVENTINTERNAL
+_ATESTLOGEVENTINTERNAL.fields_by_name['user_type'].enum_type = proto_dot_common__pb2._USERTYPE
+_ATESTLOGEVENTINTERNAL.fields_by_name['atest_start_event'].message_type = _ATESTLOGEVENTINTERNAL_ATESTSTARTEVENT
+_ATESTLOGEVENTINTERNAL.fields_by_name['atest_exit_event'].message_type = _ATESTLOGEVENTINTERNAL_ATESTEXITEVENT
+_ATESTLOGEVENTINTERNAL.fields_by_name['find_test_finish_event'].message_type = _ATESTLOGEVENTINTERNAL_FINDTESTFINISHEVENT
+_ATESTLOGEVENTINTERNAL.fields_by_name['build_finish_event'].message_type = _ATESTLOGEVENTINTERNAL_BUILDFINISHEVENT
+_ATESTLOGEVENTINTERNAL.fields_by_name['runner_finish_event'].message_type = _ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT
+_ATESTLOGEVENTINTERNAL.fields_by_name['run_tests_finish_event'].message_type = _ATESTLOGEVENTINTERNAL_RUNTESTSFINISHEVENT
+_ATESTLOGEVENTINTERNAL.fields_by_name['local_detect_event'].message_type = _ATESTLOGEVENTINTERNAL_LOCALDETECTEVENT
+_ATESTLOGEVENTINTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTINTERNAL.fields_by_name['atest_start_event'])
+_ATESTLOGEVENTINTERNAL.fields_by_name['atest_start_event'].containing_oneof = _ATESTLOGEVENTINTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTINTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTINTERNAL.fields_by_name['atest_exit_event'])
+_ATESTLOGEVENTINTERNAL.fields_by_name['atest_exit_event'].containing_oneof = _ATESTLOGEVENTINTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTINTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTINTERNAL.fields_by_name['find_test_finish_event'])
+_ATESTLOGEVENTINTERNAL.fields_by_name['find_test_finish_event'].containing_oneof = _ATESTLOGEVENTINTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTINTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTINTERNAL.fields_by_name['build_finish_event'])
+_ATESTLOGEVENTINTERNAL.fields_by_name['build_finish_event'].containing_oneof = _ATESTLOGEVENTINTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTINTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTINTERNAL.fields_by_name['runner_finish_event'])
+_ATESTLOGEVENTINTERNAL.fields_by_name['runner_finish_event'].containing_oneof = _ATESTLOGEVENTINTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTINTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTINTERNAL.fields_by_name['run_tests_finish_event'])
+_ATESTLOGEVENTINTERNAL.fields_by_name['run_tests_finish_event'].containing_oneof = _ATESTLOGEVENTINTERNAL.oneofs_by_name['event']
+_ATESTLOGEVENTINTERNAL.oneofs_by_name['event'].fields.append(
+  _ATESTLOGEVENTINTERNAL.fields_by_name['local_detect_event'])
+_ATESTLOGEVENTINTERNAL.fields_by_name['local_detect_event'].containing_oneof = _ATESTLOGEVENTINTERNAL.oneofs_by_name['event']
+DESCRIPTOR.message_types_by_name['AtestLogEventInternal'] = _ATESTLOGEVENTINTERNAL
+
+AtestLogEventInternal = _reflection.GeneratedProtocolMessageType('AtestLogEventInternal', (_message.Message,), dict(
+
+  AtestStartEvent = _reflection.GeneratedProtocolMessageType('AtestStartEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTINTERNAL_ATESTSTARTEVENT,
+    __module__ = 'proto.internal_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventInternal.AtestStartEvent)
+    ))
+  ,
+
+  AtestExitEvent = _reflection.GeneratedProtocolMessageType('AtestExitEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTINTERNAL_ATESTEXITEVENT,
+    __module__ = 'proto.internal_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventInternal.AtestExitEvent)
+    ))
+  ,
+
+  FindTestFinishEvent = _reflection.GeneratedProtocolMessageType('FindTestFinishEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTINTERNAL_FINDTESTFINISHEVENT,
+    __module__ = 'proto.internal_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventInternal.FindTestFinishEvent)
+    ))
+  ,
+
+  BuildFinishEvent = _reflection.GeneratedProtocolMessageType('BuildFinishEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTINTERNAL_BUILDFINISHEVENT,
+    __module__ = 'proto.internal_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventInternal.BuildFinishEvent)
+    ))
+  ,
+
+  RunnerFinishEvent = _reflection.GeneratedProtocolMessageType('RunnerFinishEvent', (_message.Message,), dict(
+
+    Test = _reflection.GeneratedProtocolMessageType('Test', (_message.Message,), dict(
+      DESCRIPTOR = _ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT_TEST,
+      __module__ = 'proto.internal_user_log_pb2'
+      # @@protoc_insertion_point(class_scope:AtestLogEventInternal.RunnerFinishEvent.Test)
+      ))
+    ,
+    DESCRIPTOR = _ATESTLOGEVENTINTERNAL_RUNNERFINISHEVENT,
+    __module__ = 'proto.internal_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventInternal.RunnerFinishEvent)
+    ))
+  ,
+
+  RunTestsFinishEvent = _reflection.GeneratedProtocolMessageType('RunTestsFinishEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTINTERNAL_RUNTESTSFINISHEVENT,
+    __module__ = 'proto.internal_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventInternal.RunTestsFinishEvent)
+    ))
+  ,
+
+  LocalDetectEvent = _reflection.GeneratedProtocolMessageType('LocalDetectEvent', (_message.Message,), dict(
+    DESCRIPTOR = _ATESTLOGEVENTINTERNAL_LOCALDETECTEVENT,
+    __module__ = 'proto.internal_user_log_pb2'
+    # @@protoc_insertion_point(class_scope:AtestLogEventInternal.LocalDetectEvent)
+    ))
+  ,
+  DESCRIPTOR = _ATESTLOGEVENTINTERNAL,
+  __module__ = 'proto.internal_user_log_pb2'
+  # @@protoc_insertion_point(class_scope:AtestLogEventInternal)
+  ))
+_sym_db.RegisterMessage(AtestLogEventInternal)
+_sym_db.RegisterMessage(AtestLogEventInternal.AtestStartEvent)
+_sym_db.RegisterMessage(AtestLogEventInternal.AtestExitEvent)
+_sym_db.RegisterMessage(AtestLogEventInternal.FindTestFinishEvent)
+_sym_db.RegisterMessage(AtestLogEventInternal.BuildFinishEvent)
+_sym_db.RegisterMessage(AtestLogEventInternal.RunnerFinishEvent)
+_sym_db.RegisterMessage(AtestLogEventInternal.RunnerFinishEvent.Test)
+_sym_db.RegisterMessage(AtestLogEventInternal.RunTestsFinishEvent)
+_sym_db.RegisterMessage(AtestLogEventInternal.LocalDetectEvent)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/atest/result_reporter.py b/atest/result_reporter.py
new file mode 100644
index 0000000..16da628
--- /dev/null
+++ b/atest/result_reporter.py
@@ -0,0 +1,406 @@
+# Copyright 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.
+"""
+Result Reporter
+
+The result reporter formats and prints test results.
+
+----
+Example Output for command to run following tests:
+CtsAnimationTestCases:EvaluatorTest, HelloWorldTests, and WmTests
+
+Running Tests ...
+
+CtsAnimationTestCases
+---------------------
+
+android.animation.cts.EvaluatorTest.UnitTests (7 Tests)
+[1/7] android.animation.cts.EvaluatorTest#testRectEvaluator: PASSED (153ms)
+[2/7] android.animation.cts.EvaluatorTest#testIntArrayEvaluator: PASSED (0ms)
+[3/7] android.animation.cts.EvaluatorTest#testIntEvaluator: PASSED (0ms)
+[4/7] android.animation.cts.EvaluatorTest#testFloatArrayEvaluator: PASSED (1ms)
+[5/7] android.animation.cts.EvaluatorTest#testPointFEvaluator: PASSED (1ms)
+[6/7] android.animation.cts.EvaluatorTest#testArgbEvaluator: PASSED (0ms)
+[7/7] android.animation.cts.EvaluatorTest#testFloatEvaluator: PASSED (1ms)
+
+HelloWorldTests
+---------------
+
+android.test.example.helloworld.UnitTests(2 Tests)
+[1/2] android.test.example.helloworld.HelloWorldTest#testHalloWelt: PASSED (0ms)
+[2/2] android.test.example.helloworld.HelloWorldTest#testHelloWorld: PASSED (1ms)
+
+WmTests
+-------
+
+com.android.tradefed.targetprep.UnitTests (1 Test)
+RUNNER ERROR: com.android.tradefed.targetprep.TargetSetupError:
+Failed to install WmTests.apk on 127.0.0.1:54373. Reason:
+    error message ...
+
+
+Summary
+-------
+CtsAnimationTestCases: Passed: 7, Failed: 0
+HelloWorldTests: Passed: 2, Failed: 0
+WmTests: Passed: 0, Failed: 0 (Completed With ERRORS)
+
+1 test failed
+"""
+
+from __future__ import print_function
+from collections import OrderedDict
+
+import constants
+import atest_utils as au
+
+from test_runners import test_runner_base
+
+UNSUPPORTED_FLAG = 'UNSUPPORTED_RUNNER'
+FAILURE_FLAG = 'RUNNER_FAILURE'
+
+
+class RunStat(object):
+    """Class for storing stats of a test run."""
+
+    def __init__(self, passed=0, failed=0, ignored=0, run_errors=False,
+                 assumption_failed=0):
+        """Initialize a new instance of RunStat class.
+
+        Args:
+            passed: Count of passing tests.
+            failed: Count of failed tests.
+            ignored: Count of ignored tests.
+            assumption_failed: Count of assumption failure tests.
+            run_errors: A boolean if there were run errors
+        """
+        # TODO(b/109822985): Track group and run estimated totals for updating
+        # summary line
+        self.passed = passed
+        self.failed = failed
+        self.ignored = ignored
+        self.assumption_failed = assumption_failed
+        # Run errors are not for particular tests, they are runner errors.
+        self.run_errors = run_errors
+
+    @property
+    def total(self):
+        """Getter for total tests actually ran. Accessed via self.total"""
+        return self.passed + self.failed
+
+
+class ResultReporter(object):
+    """Result Reporter class.
+
+    As each test is run, the test runner will call self.process_test_result()
+    with a TestResult namedtuple that contains the following information:
+    - runner_name:   Name of the test runner
+    - group_name:    Name of the test group if any.
+                     In Tradefed that's the Module name.
+    - test_name:     Name of the test.
+                     In Tradefed that's qualified.class#Method
+    - status:        The strings FAILED or PASSED.
+    - stacktrace:    The stacktrace if the test failed.
+    - group_total:   The total tests scheduled to be run for a group.
+                     In Tradefed this is provided when the Module starts.
+    - runner_total:  The total tests scheduled to be run for the runner.
+                     In Tradefed this is not available so is None.
+
+    The Result Reporter will print the results of this test and then update
+    its stats state.
+
+    Test stats are stored in the following structure:
+    - self.run_stats: Is RunStat instance containing stats for the overall run.
+                      This include pass/fail counts across ALL test runners.
+
+    - self.runners:  Is of the form: {RunnerName: {GroupName: RunStat Instance}}
+                     Where {} is an ordered dict.
+
+                     The stats instance contains stats for each test group.
+                     If the runner doesn't support groups, then the group
+                     name will be None.
+
+    For example this could be a state of ResultReporter:
+
+    run_stats: RunStat(passed:10, failed:5)
+    runners: {'AtestTradefedTestRunner':
+                            {'Module1': RunStat(passed:1, failed:1),
+                             'Module2': RunStat(passed:0, failed:4)},
+              'RobolectricTestRunner': {None: RunStat(passed:5, failed:0)},
+              'VtsTradefedTestRunner': {'Module1': RunStat(passed:4, failed:0)}}
+    """
+
+    def __init__(self, silent=False):
+        """Init ResultReporter.
+
+        Args:
+            silent: A boolean of silence or not.
+        """
+        self.run_stats = RunStat()
+        self.runners = OrderedDict()
+        self.failed_tests = []
+        self.all_test_results = []
+        self.pre_test = None
+        self.log_path = None
+        self.silent = silent
+        self.rerun_options = ''
+
+    def process_test_result(self, test):
+        """Given the results of a single test, update stats and print results.
+
+        Args:
+            test: A TestResult namedtuple.
+        """
+        if test.runner_name not in self.runners:
+            self.runners[test.runner_name] = OrderedDict()
+        assert self.runners[test.runner_name] != FAILURE_FLAG
+        self.all_test_results.append(test)
+        if test.group_name not in self.runners[test.runner_name]:
+            self.runners[test.runner_name][test.group_name] = RunStat()
+            self._print_group_title(test)
+        self._update_stats(test,
+                           self.runners[test.runner_name][test.group_name])
+        self._print_result(test)
+
+    def runner_failure(self, runner_name, failure_msg):
+        """Report a runner failure.
+
+        Use instead of process_test_result() when runner fails separate from
+        any particular test, e.g. during setup of runner.
+
+        Args:
+            runner_name: A string of the name of the runner.
+            failure_msg: A string of the failure message to pass to user.
+        """
+        self.runners[runner_name] = FAILURE_FLAG
+        print('\n', runner_name, '\n', '-' * len(runner_name), sep='')
+        print('Runner encountered a critical failure. Skipping.\n'
+              'FAILURE: %s' % failure_msg)
+
+    def register_unsupported_runner(self, runner_name):
+        """Register an unsupported runner.
+
+           Prints the following to the screen:
+
+           RunnerName
+           ----------
+           This runner does not support normal results formatting.
+           Below is the raw output of the test runner.
+
+           RAW OUTPUT:
+           <Raw Runner Output>
+
+           Args:
+              runner_name: A String of the test runner's name.
+        """
+        assert runner_name not in self.runners
+        self.runners[runner_name] = UNSUPPORTED_FLAG
+        print('\n', runner_name, '\n', '-' * len(runner_name), sep='')
+        print('This runner does not support normal results formatting. Below '
+              'is the raw output of the test runner.\n\nRAW OUTPUT:')
+
+    def print_starting_text(self):
+        """Print starting text for running tests."""
+        print(au.colorize('\nRunning Tests...', constants.CYAN))
+
+    def print_summary(self):
+        """Print summary of all test runs.
+
+        Returns:
+            0 if all tests pass, non-zero otherwise.
+
+        """
+        tests_ret = constants.EXIT_CODE_SUCCESS
+        if not self.runners:
+            return tests_ret
+        print('\n%s' % au.colorize('Summary', constants.CYAN))
+        print('-------')
+        if self.rerun_options:
+            print(self.rerun_options)
+        failed_sum = len(self.failed_tests)
+        for runner_name, groups in self.runners.items():
+            if groups == UNSUPPORTED_FLAG:
+                print(runner_name, 'Unsupported. See raw output above.')
+                continue
+            if groups == FAILURE_FLAG:
+                tests_ret = constants.EXIT_CODE_TEST_FAILURE
+                print(runner_name, 'Crashed. No results to report.')
+                failed_sum += 1
+                continue
+            for group_name, stats in groups.items():
+                name = group_name if group_name else runner_name
+                summary = self.process_summary(name, stats)
+                if stats.failed > 0:
+                    tests_ret = constants.EXIT_CODE_TEST_FAILURE
+                if stats.run_errors:
+                    tests_ret = constants.EXIT_CODE_TEST_FAILURE
+                    failed_sum += 1 if not stats.failed else 0
+                print(summary)
+        print()
+        if tests_ret == constants.EXIT_CODE_SUCCESS:
+            print(au.colorize('All tests passed!', constants.GREEN))
+        else:
+            message = '%d %s failed' % (failed_sum,
+                                        'tests' if failed_sum > 1 else 'test')
+            print(au.colorize(message, constants.RED))
+            print('-'*len(message))
+            self.print_failed_tests()
+        if self.log_path:
+            print('Test Logs have saved in %s' % self.log_path)
+        return tests_ret
+
+    def print_failed_tests(self):
+        """Print the failed tests if existed."""
+        if self.failed_tests:
+            for test_name in self.failed_tests:
+                print('%s' % test_name)
+
+    def process_summary(self, name, stats):
+        """Process the summary line.
+
+        Strategy:
+            Error status happens ->
+                SomeTests: Passed: 2, Failed: 0 <red>(Completed With ERRORS)</red>
+                SomeTests: Passed: 2, <red>Failed</red>: 2 <red>(Completed With ERRORS)</red>
+            More than 1 test fails ->
+                SomeTests: Passed: 2, <red>Failed</red>: 5
+            No test fails ->
+                SomeTests: <green>Passed</green>: 2, Failed: 0
+
+        Args:
+            name: A string of test name.
+            stats: A RunStat instance for a test group.
+
+        Returns:
+            A summary of the test result.
+        """
+        passed_label = 'Passed'
+        failed_label = 'Failed'
+        ignored_label = 'Ignored'
+        assumption_failed_label = 'Assumption Failed'
+        error_label = ''
+        if stats.failed > 0:
+            failed_label = au.colorize(failed_label, constants.RED)
+        if stats.run_errors:
+            error_label = au.colorize('(Completed With ERRORS)', constants.RED)
+        elif stats.failed == 0:
+            passed_label = au.colorize(passed_label, constants.GREEN)
+        summary = '%s: %s: %s, %s: %s, %s: %s, %s: %s %s' % (name,
+                                                             passed_label,
+                                                             stats.passed,
+                                                             failed_label,
+                                                             stats.failed,
+                                                             ignored_label,
+                                                             stats.ignored,
+                                                             assumption_failed_label,
+                                                             stats.assumption_failed,
+                                                             error_label)
+        return summary
+
+    def _update_stats(self, test, group):
+        """Given the results of a single test, update test run stats.
+
+        Args:
+            test: a TestResult namedtuple.
+            group: a RunStat instance for a test group.
+        """
+        # TODO(109822985): Track group and run estimated totals for updating
+        # summary line
+        if test.status == test_runner_base.PASSED_STATUS:
+            self.run_stats.passed += 1
+            group.passed += 1
+        elif test.status == test_runner_base.IGNORED_STATUS:
+            self.run_stats.ignored += 1
+            group.ignored += 1
+        elif test.status == test_runner_base.ASSUMPTION_FAILED:
+            self.run_stats.assumption_failed += 1
+            group.assumption_failed += 1
+        elif test.status == test_runner_base.FAILED_STATUS:
+            self.run_stats.failed += 1
+            self.failed_tests.append(test.test_name)
+            group.failed += 1
+        elif test.status == test_runner_base.ERROR_STATUS:
+            self.run_stats.run_errors = True
+            group.run_errors = True
+
+    def _print_group_title(self, test):
+        """Print the title line for a test group.
+
+        Test Group/Runner Name
+        ----------------------
+
+        Args:
+            test: A TestResult namedtuple.
+        """
+        if self.silent:
+            return
+        title = test.group_name or test.runner_name
+        underline = '-' * (len(title))
+        print('\n%s\n%s' % (title, underline))
+
+    def _print_result(self, test):
+        """Print the results of a single test.
+
+           Looks like:
+           fully.qualified.class#TestMethod: PASSED/FAILED
+
+        Args:
+            test: a TestResult namedtuple.
+        """
+        if self.silent:
+            return
+        if not self.pre_test or (test.test_run_name !=
+                                 self.pre_test.test_run_name):
+            print('%s (%s %s)' % (au.colorize(test.test_run_name,
+                                              constants.BLUE),
+                                  test.group_total,
+                                  'Test' if test.group_total <= 1 else 'Tests'))
+        if test.status == test_runner_base.ERROR_STATUS:
+            print('RUNNER ERROR: %s\n' % test.details)
+            self.pre_test = test
+            return
+        if test.test_name:
+            if test.status == test_runner_base.PASSED_STATUS:
+                # Example of output:
+                # [78/92] test_name: PASSED (92ms)
+                print('[%s/%s] %s: %s %s' % (test.test_count,
+                                             test.group_total,
+                                             test.test_name,
+                                             au.colorize(
+                                                 test.status,
+                                                 constants.GREEN),
+                                             test.test_time))
+                for key, data in test.additional_info.items():
+                    print('\t%s: %s' % (au.colorize(key, constants.BLUE), data))
+            elif test.status == test_runner_base.IGNORED_STATUS:
+                # Example: [33/92] test_name: IGNORED (12ms)
+                print('[%s/%s] %s: %s %s' % (test.test_count, test.group_total,
+                                             test.test_name, au.colorize(
+                                                 test.status, constants.MAGENTA),
+                                             test.test_time))
+            elif test.status == test_runner_base.ASSUMPTION_FAILED:
+                # Example: [33/92] test_name: ASSUMPTION_FAILED (12ms)
+                print('[%s/%s] %s: %s %s' % (test.test_count, test.group_total,
+                                             test.test_name, au.colorize(
+                                                 test.status, constants.MAGENTA),
+                                             test.test_time))
+            else:
+                # Example: [26/92] test_name: FAILED (32ms)
+                print('[%s/%s] %s: %s %s' % (test.test_count, test.group_total,
+                                             test.test_name, au.colorize(
+                                                 test.status, constants.RED),
+                                             test.test_time))
+        if test.status == test_runner_base.FAILED_STATUS:
+            print('\nSTACKTRACE:\n%s' % test.details)
+        self.pre_test = test
diff --git a/atest/result_reporter_unittest.py b/atest/result_reporter_unittest.py
new file mode 100755
index 0000000..1a5221e
--- /dev/null
+++ b/atest/result_reporter_unittest.py
@@ -0,0 +1,371 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for result_reporter."""
+
+import sys
+import unittest
+import mock
+
+import result_reporter
+from test_runners import test_runner_base
+
+if sys.version_info[0] == 2:
+    from StringIO import StringIO
+else:
+    from io import StringIO
+
+RESULT_PASSED_TEST = test_runner_base.TestResult(
+    runner_name='someTestRunner',
+    group_name='someTestModule',
+    test_name='someClassName#sostName',
+    status=test_runner_base.PASSED_STATUS,
+    details=None,
+    test_count=1,
+    test_time='(10ms)',
+    runner_total=None,
+    group_total=2,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+RESULT_PASSED_TEST_MODULE_2 = test_runner_base.TestResult(
+    runner_name='someTestRunner',
+    group_name='someTestModule2',
+    test_name='someClassName#sostName',
+    status=test_runner_base.PASSED_STATUS,
+    details=None,
+    test_count=1,
+    test_time='(10ms)',
+    runner_total=None,
+    group_total=2,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+RESULT_PASSED_TEST_RUNNER_2_NO_MODULE = test_runner_base.TestResult(
+    runner_name='someTestRunner2',
+    group_name=None,
+    test_name='someClassName#sostName',
+    status=test_runner_base.PASSED_STATUS,
+    details=None,
+    test_count=1,
+    test_time='(10ms)',
+    runner_total=None,
+    group_total=2,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+RESULT_FAILED_TEST = test_runner_base.TestResult(
+    runner_name='someTestRunner',
+    group_name='someTestModule',
+    test_name='someClassName2#sestName2',
+    status=test_runner_base.FAILED_STATUS,
+    details='someTrace',
+    test_count=1,
+    test_time='',
+    runner_total=None,
+    group_total=2,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+RESULT_RUN_FAILURE = test_runner_base.TestResult(
+    runner_name='someTestRunner',
+    group_name='someTestModule',
+    test_name='someClassName#sostName',
+    status=test_runner_base.ERROR_STATUS,
+    details='someRunFailureReason',
+    test_count=1,
+    test_time='',
+    runner_total=None,
+    group_total=2,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+RESULT_INVOCATION_FAILURE = test_runner_base.TestResult(
+    runner_name='someTestRunner',
+    group_name=None,
+    test_name=None,
+    status=test_runner_base.ERROR_STATUS,
+    details='someInvocationFailureReason',
+    test_count=1,
+    test_time='',
+    runner_total=None,
+    group_total=None,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+RESULT_IGNORED_TEST = test_runner_base.TestResult(
+    runner_name='someTestRunner',
+    group_name='someTestModule',
+    test_name='someClassName#sostName',
+    status=test_runner_base.IGNORED_STATUS,
+    details=None,
+    test_count=1,
+    test_time='(10ms)',
+    runner_total=None,
+    group_total=2,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+RESULT_ASSUMPTION_FAILED_TEST = test_runner_base.TestResult(
+    runner_name='someTestRunner',
+    group_name='someTestModule',
+    test_name='someClassName#sostName',
+    status=test_runner_base.ASSUMPTION_FAILED,
+    details=None,
+    test_count=1,
+    test_time='(10ms)',
+    runner_total=None,
+    group_total=2,
+    additional_info={},
+    test_run_name='com.android.UnitTests'
+)
+
+#pylint: disable=protected-access
+#pylint: disable=invalid-name
+class ResultReporterUnittests(unittest.TestCase):
+    """Unit tests for result_reporter.py"""
+
+    def setUp(self):
+        self.rr = result_reporter.ResultReporter()
+
+    def tearDown(self):
+        mock.patch.stopall()
+
+    @mock.patch.object(result_reporter.ResultReporter, '_print_group_title')
+    @mock.patch.object(result_reporter.ResultReporter, '_update_stats')
+    @mock.patch.object(result_reporter.ResultReporter, '_print_result')
+    def test_process_test_result(self, mock_print, mock_update, mock_title):
+        """Test process_test_result method."""
+        # Passed Test
+        self.assertTrue('someTestRunner' not in self.rr.runners)
+        self.rr.process_test_result(RESULT_PASSED_TEST)
+        self.assertTrue('someTestRunner' in self.rr.runners)
+        group = self.rr.runners['someTestRunner'].get('someTestModule')
+        self.assertIsNotNone(group)
+        mock_title.assert_called_with(RESULT_PASSED_TEST)
+        mock_update.assert_called_with(RESULT_PASSED_TEST, group)
+        mock_print.assert_called_with(RESULT_PASSED_TEST)
+        # Failed Test
+        mock_title.reset_mock()
+        self.rr.process_test_result(RESULT_FAILED_TEST)
+        mock_title.assert_not_called()
+        mock_update.assert_called_with(RESULT_FAILED_TEST, group)
+        mock_print.assert_called_with(RESULT_FAILED_TEST)
+        # Test with new Group
+        mock_title.reset_mock()
+        self.rr.process_test_result(RESULT_PASSED_TEST_MODULE_2)
+        self.assertTrue('someTestModule2' in self.rr.runners['someTestRunner'])
+        mock_title.assert_called_with(RESULT_PASSED_TEST_MODULE_2)
+        # Test with new Runner
+        mock_title.reset_mock()
+        self.rr.process_test_result(RESULT_PASSED_TEST_RUNNER_2_NO_MODULE)
+        self.assertTrue('someTestRunner2' in self.rr.runners)
+        mock_title.assert_called_with(RESULT_PASSED_TEST_RUNNER_2_NO_MODULE)
+
+    def test_print_result_run_name(self):
+        """Test print run name function in print_result method."""
+        try:
+            rr = result_reporter.ResultReporter()
+            capture_output = StringIO()
+            sys.stdout = capture_output
+            run_name = 'com.android.UnitTests'
+            rr._print_result(test_runner_base.TestResult(
+                runner_name='runner_name',
+                group_name='someTestModule',
+                test_name='someClassName#someTestName',
+                status=test_runner_base.FAILED_STATUS,
+                details='someTrace',
+                test_count=2,
+                test_time='(2h44m36.402s)',
+                runner_total=None,
+                group_total=2,
+                additional_info={},
+                test_run_name=run_name
+            ))
+            # Make sure run name in the first line.
+            capture_output_str = capture_output.getvalue().strip()
+            self.assertTrue(run_name in capture_output_str.split('\n')[0])
+            run_name2 = 'com.android.UnitTests2'
+            capture_output = StringIO()
+            sys.stdout = capture_output
+            rr._print_result(test_runner_base.TestResult(
+                runner_name='runner_name',
+                group_name='someTestModule',
+                test_name='someClassName#someTestName',
+                status=test_runner_base.FAILED_STATUS,
+                details='someTrace',
+                test_count=2,
+                test_time='(2h43m36.402s)',
+                runner_total=None,
+                group_total=2,
+                additional_info={},
+                test_run_name=run_name2
+            ))
+            # Make sure run name in the first line.
+            capture_output_str = capture_output.getvalue().strip()
+            self.assertTrue(run_name2 in capture_output_str.split('\n')[0])
+        finally:
+            sys.stdout = sys.__stdout__
+
+    def test_register_unsupported_runner(self):
+        """Test register_unsupported_runner method."""
+        self.rr.register_unsupported_runner('NotSupported')
+        runner = self.rr.runners['NotSupported']
+        self.assertIsNotNone(runner)
+        self.assertEquals(runner, result_reporter.UNSUPPORTED_FLAG)
+
+    def test_update_stats_passed(self):
+        """Test _update_stats method."""
+        # Passed Test
+        group = result_reporter.RunStat()
+        self.rr._update_stats(RESULT_PASSED_TEST, group)
+        self.assertEquals(self.rr.run_stats.passed, 1)
+        self.assertEquals(self.rr.run_stats.failed, 0)
+        self.assertEquals(self.rr.run_stats.run_errors, False)
+        self.assertEquals(self.rr.failed_tests, [])
+        self.assertEquals(group.passed, 1)
+        self.assertEquals(group.failed, 0)
+        self.assertEquals(group.ignored, 0)
+        self.assertEquals(group.run_errors, False)
+        # Passed Test New Group
+        group2 = result_reporter.RunStat()
+        self.rr._update_stats(RESULT_PASSED_TEST_MODULE_2, group2)
+        self.assertEquals(self.rr.run_stats.passed, 2)
+        self.assertEquals(self.rr.run_stats.failed, 0)
+        self.assertEquals(self.rr.run_stats.run_errors, False)
+        self.assertEquals(self.rr.failed_tests, [])
+        self.assertEquals(group2.passed, 1)
+        self.assertEquals(group2.failed, 0)
+        self.assertEquals(group.ignored, 0)
+        self.assertEquals(group2.run_errors, False)
+
+    def test_update_stats_failed(self):
+        """Test _update_stats method."""
+        # Passed Test
+        group = result_reporter.RunStat()
+        self.rr._update_stats(RESULT_PASSED_TEST, group)
+        # Passed Test New Group
+        group2 = result_reporter.RunStat()
+        self.rr._update_stats(RESULT_PASSED_TEST_MODULE_2, group2)
+        # Failed Test Old Group
+        self.rr._update_stats(RESULT_FAILED_TEST, group)
+        self.assertEquals(self.rr.run_stats.passed, 2)
+        self.assertEquals(self.rr.run_stats.failed, 1)
+        self.assertEquals(self.rr.run_stats.run_errors, False)
+        self.assertEquals(self.rr.failed_tests, [RESULT_FAILED_TEST.test_name])
+        self.assertEquals(group.passed, 1)
+        self.assertEquals(group.failed, 1)
+        self.assertEquals(group.ignored, 0)
+        self.assertEquals(group.total, 2)
+        self.assertEquals(group2.total, 1)
+        self.assertEquals(group.run_errors, False)
+        # Test Run Failure
+        self.rr._update_stats(RESULT_RUN_FAILURE, group)
+        self.assertEquals(self.rr.run_stats.passed, 2)
+        self.assertEquals(self.rr.run_stats.failed, 1)
+        self.assertEquals(self.rr.run_stats.run_errors, True)
+        self.assertEquals(self.rr.failed_tests, [RESULT_FAILED_TEST.test_name])
+        self.assertEquals(group.passed, 1)
+        self.assertEquals(group.failed, 1)
+        self.assertEquals(group.ignored, 0)
+        self.assertEquals(group.run_errors, True)
+        self.assertEquals(group2.run_errors, False)
+        # Invocation Failure
+        self.rr._update_stats(RESULT_INVOCATION_FAILURE, group)
+        self.assertEquals(self.rr.run_stats.passed, 2)
+        self.assertEquals(self.rr.run_stats.failed, 1)
+        self.assertEquals(self.rr.run_stats.run_errors, True)
+        self.assertEquals(self.rr.failed_tests, [RESULT_FAILED_TEST.test_name])
+        self.assertEquals(group.passed, 1)
+        self.assertEquals(group.failed, 1)
+        self.assertEquals(group.ignored, 0)
+        self.assertEquals(group.run_errors, True)
+
+    def test_update_stats_ignored_and_assumption_failure(self):
+        """Test _update_stats method."""
+        # Passed Test
+        group = result_reporter.RunStat()
+        self.rr._update_stats(RESULT_PASSED_TEST, group)
+        # Passed Test New Group
+        group2 = result_reporter.RunStat()
+        self.rr._update_stats(RESULT_PASSED_TEST_MODULE_2, group2)
+        # Failed Test Old Group
+        self.rr._update_stats(RESULT_FAILED_TEST, group)
+        # Test Run Failure
+        self.rr._update_stats(RESULT_RUN_FAILURE, group)
+        # Invocation Failure
+        self.rr._update_stats(RESULT_INVOCATION_FAILURE, group)
+        # Ignored Test
+        self.rr._update_stats(RESULT_IGNORED_TEST, group)
+        self.assertEquals(self.rr.run_stats.passed, 2)
+        self.assertEquals(self.rr.run_stats.failed, 1)
+        self.assertEquals(self.rr.run_stats.run_errors, True)
+        self.assertEquals(self.rr.failed_tests, [RESULT_FAILED_TEST.test_name])
+        self.assertEquals(group.passed, 1)
+        self.assertEquals(group.failed, 1)
+        self.assertEquals(group.ignored, 1)
+        self.assertEquals(group.run_errors, True)
+        # 2nd Ignored Test
+        self.rr._update_stats(RESULT_IGNORED_TEST, group)
+        self.assertEquals(self.rr.run_stats.passed, 2)
+        self.assertEquals(self.rr.run_stats.failed, 1)
+        self.assertEquals(self.rr.run_stats.run_errors, True)
+        self.assertEquals(self.rr.failed_tests, [RESULT_FAILED_TEST.test_name])
+        self.assertEquals(group.passed, 1)
+        self.assertEquals(group.failed, 1)
+        self.assertEquals(group.ignored, 2)
+        self.assertEquals(group.run_errors, True)
+
+        # Assumption_Failure test
+        self.rr._update_stats(RESULT_ASSUMPTION_FAILED_TEST, group)
+        self.assertEquals(group.assumption_failed, 1)
+        # 2nd Assumption_Failure test
+        self.rr._update_stats(RESULT_ASSUMPTION_FAILED_TEST, group)
+        self.assertEquals(group.assumption_failed, 2)
+
+    def test_print_summary_ret_val(self):
+        """Test print_summary method's return value."""
+        # PASS Case
+        self.rr.process_test_result(RESULT_PASSED_TEST)
+        self.assertEquals(0, self.rr.print_summary())
+        # PASS Case + Fail Case
+        self.rr.process_test_result(RESULT_FAILED_TEST)
+        self.assertNotEqual(0, self.rr.print_summary())
+        # PASS Case + Fail Case + PASS Case
+        self.rr.process_test_result(RESULT_PASSED_TEST_MODULE_2)
+        self.assertNotEqual(0, self.rr.print_summary())
+
+    def test_print_summary_ret_val_err_stat(self):
+        """Test print_summary method's return value."""
+        # PASS Case
+        self.rr.process_test_result(RESULT_PASSED_TEST)
+        self.assertEquals(0, self.rr.print_summary())
+        # PASS Case + Fail Case
+        self.rr.process_test_result(RESULT_RUN_FAILURE)
+        self.assertNotEqual(0, self.rr.print_summary())
+        # PASS Case + Fail Case + PASS Case
+        self.rr.process_test_result(RESULT_PASSED_TEST_MODULE_2)
+        self.assertNotEqual(0, self.rr.print_summary())
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/run_atest_unittests.sh b/atest/run_atest_unittests.sh
new file mode 100755
index 0000000..db28ac5
--- /dev/null
+++ b/atest/run_atest_unittests.sh
@@ -0,0 +1,83 @@
+#!/bin/bash
+
+# Copyright (C) 2017 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 simple helper script that runs all of the atest unit tests.
+# There are 2 situations that we take care of:
+#   1. User wants to invoke this script directly.
+#   2. PREUPLOAD hook invokes this script.
+
+ATEST_DIR=$(dirname $0)
+[ "$(uname -s)" == "Darwin" ] && { realpath(){ echo "$(cd $(dirname $1);pwd -P)/$(basename $1)"; }; }
+ATEST_REAL_PATH=$(realpath $ATEST_DIR)
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+NC='\033[0m' # No Color
+COVERAGE=false
+
+function get_pythonpath() {
+    echo "$ATEST_REAL_PATH:$PYTHONPATH"
+}
+
+function print_summary() {
+    local test_results=$1
+    if [[ $COVERAGE == true ]]; then
+        coverage report -m
+        coverage html
+    fi
+    if [[ $test_results -eq 0 ]]; then
+        echo -e "${GREEN}All unittests pass${NC}!"
+    else
+        echo -e "${RED}There was a unittest failure${NC}"
+    fi
+}
+
+function run_atest_unittests() {
+    echo "Running tests..."
+    local run_cmd="python"
+    local rc=0
+    if [[ $COVERAGE == true ]]; then
+        # Clear previously coverage data.
+        python -m coverage erase
+        # Collect coverage data.
+        run_cmd="coverage run --source $ATEST_REAL_PATH --append"
+    fi
+
+    for test_file in $(find $ATEST_DIR -name "*_unittest.py"); do
+        if ! PYTHONPATH=$(get_pythonpath) $run_cmd $test_file; then
+          rc=1
+          echo -e "${RED}$t failed${NC}"
+        fi
+    done
+    echo
+    print_summary $rc
+    return $rc
+}
+
+# Let's check if anything is passed in, if not we assume the user is invoking
+# script, but if we get a list of files, assume it's the PREUPLOAD hook.
+read -ra PREUPLOAD_FILES <<< "$@"
+if [[ ${#PREUPLOAD_FILES[@]} -eq 0 ]]; then
+    run_atest_unittests; exit $?
+elif [[ "${#PREUPLOAD_FILES[@]}" -eq 1 && "${PREUPLOAD_FILES}" == "coverage" ]]; then
+    COVERAGE=true run_atest_unittests; exit $?
+else
+    for f in ${PREUPLOAD_FILES[@]}; do
+        # We only want to run this unittest if atest files have been touched.
+        if [[ $f == atest/* ]]; then
+            run_atest_unittests; exit $?
+        fi
+    done
+fi
diff --git a/atest/test_data/test_commands.json b/atest/test_data/test_commands.json
new file mode 100644
index 0000000..bdd2390
--- /dev/null
+++ b/atest/test_data/test_commands.json
@@ -0,0 +1,59 @@
+{
+"hello_world_test": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter hello_world_test --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"packages/apps/Car/Messenger/tests/robotests/src/com/android/car/messenger/MessengerDelegateTest.java": [
+"./build/soong/soong_ui.bash --make-mode RunCarMessengerRoboTests"
+], 
+"CtsAnimationTestCases:AnimatorTest": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter CtsAnimationTestCases --atest-include-filter CtsAnimationTestCases:android.animation.cts.AnimatorTest --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceReportLogTest": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceReportLogTest --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"CtsAnimationTestCases CtsSampleDeviceTestCases": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter CtsAnimationTestCases --include-filter CtsSampleDeviceTestCases --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"AnimatorTest": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter CtsAnimationTestCases --atest-include-filter CtsAnimationTestCases:android.animation.cts.AnimatorTest --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"PacketFragmenterTest": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter net_test_hci --atest-include-filter net_test_hci:PacketFragmenterTest.* --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"android.animation.cts": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter CtsAnimationTestCases --atest-include-filter CtsAnimationTestCases:android.animation.cts --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"platform_testing/tests/example/native/Android.bp": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter hello_world_test --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"tools/tradefederation/core/res/config/native-benchmark.xml": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter native-benchmark --log-level WARN --logcat-on-failure --no-enable-granular-attempts"
+], 
+"native-benchmark": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter native-benchmark --log-level WARN --logcat-on-failure --no-enable-granular-attempts"
+], 
+"platform_testing/tests/example/native": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter hello_world_test --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"VtsCodelabHelloWorldTest": [
+"vts-tradefed run commandAndExit vts-staging-default -m VtsCodelabHelloWorldTest --skip-all-system-status-check --skip-preconditions --primary-abi-only"
+], 
+"aidegen_unittests": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --atest-log-file-path=/tmp/atest_run_1568627341_v33kdA/log --include-filter aidegen_unittests --log-level WARN"
+], 
+"HelloWorldTests": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter HelloWorldTests --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"CtsSampleDeviceTestCases:SampleDeviceTest#testSharedPreferences": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts.SampleDeviceTest#testSharedPreferences --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"CtsSampleDeviceTestCases:android.sample.cts": [
+"atest_tradefed.sh template/atest_local_min --template:map test=atest --include-filter CtsSampleDeviceTestCases --atest-include-filter CtsSampleDeviceTestCases:android.sample.cts --log-level WARN --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"PacketFragmenterTest#test_no_fragment_necessary,test_ble_fragment_necessary": [
+"atest_tradefed.sh template/atest_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 --skip-loading-config-jar --logcat-on-failure --no-enable-granular-attempts"
+], 
+"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
new file mode 100644
index 0000000..360c66e
--- /dev/null
+++ b/atest/test_finder_handler.py
@@ -0,0 +1,256 @@
+# Copyright 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.
+
+"""
+Test Finder Handler module.
+"""
+
+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
+from test_finders import module_finder
+
+# List of default test finder classes.
+_TEST_FINDERS = {
+    suite_plan_finder.SuitePlanFinder,
+    tf_integration_finder.TFIntegrationFinder,
+    module_finder.ModuleFinder,
+    cache_finder.CacheFinder,
+}
+
+# Explanation of REFERENCE_TYPEs:
+# ----------------------------------
+# 0. MODULE: LOCAL_MODULE or LOCAL_PACKAGE_NAME value in Android.mk/Android.bp.
+# 1. CLASS: Names which the same with a ClassName.java/kt file.
+# 2. QUALIFIED_CLASS: String like "a.b.c.ClassName".
+# 3. MODULE_CLASS: Combo of MODULE and CLASS as "module:class".
+# 4. PACKAGE: Package in java file. Same as file path to java file.
+# 5. MODULE_PACKAGE: Combo of MODULE and PACKAGE as "module:package".
+# 6. MODULE_FILE_PATH: File path to dir of tests or test itself.
+# 7. INTEGRATION_FILE_PATH: File path to config xml in one of the 4 integration
+#                           config directories.
+# 8. INTEGRATION: xml file name in one of the 4 integration config directories.
+# 9. SUITE: Value of the "run-suite-tag" in xml config file in 4 config dirs.
+#           Same as value of "test-suite-tag" in AndroidTest.xml files.
+# 10. CC_CLASS: Test case in cc file.
+# 11. SUITE_PLAN: Suite name such as cts.
+# 12. SUITE_PLAN_FILE_PATH: File path to config xml in the suite config directories.
+# 13. CACHE: A pseudo type that runs cache_finder without finding test in real.
+_REFERENCE_TYPE = atest_enum.AtestEnum(['MODULE', 'CLASS', 'QUALIFIED_CLASS',
+                                        'MODULE_CLASS', 'PACKAGE',
+                                        'MODULE_PACKAGE', 'MODULE_FILE_PATH',
+                                        'INTEGRATION_FILE_PATH', 'INTEGRATION',
+                                        'SUITE', 'CC_CLASS', 'SUITE_PLAN',
+                                        'SUITE_PLAN_FILE_PATH', 'CACHE'])
+
+_REF_TYPE_TO_FUNC_MAP = {
+    _REFERENCE_TYPE.MODULE: module_finder.ModuleFinder.find_test_by_module_name,
+    _REFERENCE_TYPE.CLASS: module_finder.ModuleFinder.find_test_by_class_name,
+    _REFERENCE_TYPE.MODULE_CLASS: module_finder.ModuleFinder.find_test_by_module_and_class,
+    _REFERENCE_TYPE.QUALIFIED_CLASS: module_finder.ModuleFinder.find_test_by_class_name,
+    _REFERENCE_TYPE.PACKAGE: module_finder.ModuleFinder.find_test_by_package_name,
+    _REFERENCE_TYPE.MODULE_PACKAGE: module_finder.ModuleFinder.find_test_by_module_and_package,
+    _REFERENCE_TYPE.MODULE_FILE_PATH: module_finder.ModuleFinder.find_test_by_path,
+    _REFERENCE_TYPE.INTEGRATION_FILE_PATH:
+        tf_integration_finder.TFIntegrationFinder.find_int_test_by_path,
+    _REFERENCE_TYPE.INTEGRATION:
+        tf_integration_finder.TFIntegrationFinder.find_test_by_integration_name,
+    _REFERENCE_TYPE.CC_CLASS:
+        module_finder.ModuleFinder.find_test_by_cc_class_name,
+    _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,
+}
+
+
+def _get_finder_instance_dict(module_info):
+    """Return dict of finder instances.
+
+    Args:
+        module_info: ModuleInfo for finder classes to use.
+
+    Returns:
+        Dict of finder instances keyed by their name.
+    """
+    instance_dict = {}
+    for finder in _get_test_finders():
+        instance_dict[finder.NAME] = finder(module_info=module_info)
+    return instance_dict
+
+
+def _get_test_finders():
+    """Returns the test finders.
+
+    If external test types are defined outside atest, they can be try-except
+    imported into here.
+
+    Returns:
+        Set of test finder classes.
+    """
+    test_finders_list = _TEST_FINDERS
+    # Example import of external test finder:
+    try:
+        from test_finders import example_finder
+        test_finders_list.add(example_finder.ExampleFinder)
+    except ImportError:
+        pass
+    return test_finders_list
+
+# pylint: disable=too-many-return-statements
+def _get_test_reference_types(ref):
+    """Determine type of test reference based on the content of string.
+
+    Examples:
+        The string 'SequentialRWTest' could be a reference to
+        a Module or a Class name.
+
+        The string 'cts/tests/filesystem' could be a Path, Integration
+        or Suite reference.
+
+    Args:
+        ref: A string referencing a test.
+
+    Returns:
+        A list of possible REFERENCE_TYPEs (ints) for reference string.
+    """
+    if ref.startswith('.') or '..' in ref:
+        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.CACHE,
+                    _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+                    _REFERENCE_TYPE.MODULE_FILE_PATH,
+                    _REFERENCE_TYPE.SUITE_PLAN_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,
+                # TODO: Uncomment in SUITE when it's supported
+                # _REFERENCE_TYPE.SUITE
+               ]
+    if '.' in ref:
+        ref_end = ref.rsplit('.', 1)[-1]
+        ref_end_is_upper = ref_end[0].isupper()
+    if ':' in ref:
+        if '.' in ref:
+            if ref_end_is_upper:
+                # Module:fully.qualified.Class or Integration:fully.q.Class
+                return [_REFERENCE_TYPE.CACHE,
+                        _REFERENCE_TYPE.INTEGRATION,
+                        _REFERENCE_TYPE.MODULE_CLASS]
+            # Module:some.package
+            return [_REFERENCE_TYPE.CACHE, _REFERENCE_TYPE.MODULE_PACKAGE]
+        # Module:Class or IntegrationName:Class
+        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.
+        # foo.java#method, so let ref_end be the first part of splitting '#'.
+        if "#" in ref_end:
+            ref_end = ref_end.split('#')[0]
+        if ref_end in ('java', 'kt', 'bp', 'mk', 'cc', 'cpp'):
+            return [_REFERENCE_TYPE.CACHE, _REFERENCE_TYPE.MODULE_FILE_PATH]
+        if ref_end == 'xml':
+            return [_REFERENCE_TYPE.CACHE,
+                    _REFERENCE_TYPE.INTEGRATION_FILE_PATH,
+                    _REFERENCE_TYPE.SUITE_PLAN_FILE_PATH]
+        if ref_end_is_upper:
+            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.CACHE,
+            _REFERENCE_TYPE.INTEGRATION,
+            # TODO: Uncomment in SUITE when it's supported
+            # _REFERENCE_TYPE.SUITE,
+            _REFERENCE_TYPE.MODULE,
+            _REFERENCE_TYPE.SUITE_PLAN,
+            _REFERENCE_TYPE.CLASS,
+            _REFERENCE_TYPE.CC_CLASS]
+
+
+def _get_registered_find_methods(module_info):
+    """Return list of registered find methods.
+
+    This is used to return find methods that were not listed in the
+    default find methods but just registered in the finder classes. These
+    find methods will run before the default find methods.
+
+    Args:
+        module_info: ModuleInfo for finder classes to instantiate with.
+
+    Returns:
+        List of registered find methods.
+    """
+    find_methods = []
+    finder_instance_dict = _get_finder_instance_dict(module_info)
+    for finder in _get_test_finders():
+        finder_instance = finder_instance_dict[finder.NAME]
+        for find_method_info in finder_instance.get_all_find_methods():
+            find_methods.append(test_finder_base.Finder(
+                finder_instance, find_method_info.find_method, finder.NAME))
+    return find_methods
+
+
+def _get_default_find_methods(module_info, test):
+    """Default find methods to be used based on the given test name.
+
+    Args:
+        module_info: ModuleInfo for finder instances to use.
+        test: String of test name to help determine which find methods
+              to utilize.
+
+    Returns:
+        List of find methods to use.
+    """
+    find_methods = []
+    finder_instance_dict = _get_finder_instance_dict(module_info)
+    test_ref_types = _get_test_reference_types(test)
+    logging.debug('Resolved input to possible references: %s', [
+        _REFERENCE_TYPE[t] for t in test_ref_types])
+    for test_ref_type in test_ref_types:
+        find_method = _REF_TYPE_TO_FUNC_MAP[test_ref_type]
+        finder_instance = finder_instance_dict[find_method.im_class.NAME]
+        finder_info = _REFERENCE_TYPE[test_ref_type]
+        find_methods.append(test_finder_base.Finder(finder_instance,
+                                                    find_method,
+                                                    finder_info))
+    return find_methods
+
+
+def get_find_methods_for_test(module_info, test):
+    """Return a list of ordered find methods.
+
+    Args:
+      test: String of test name to get find methods for.
+
+    Returns:
+        List of ordered find methods.
+    """
+    registered_find_methods = _get_registered_find_methods(module_info)
+    default_find_methods = _get_default_find_methods(module_info, test)
+    return registered_find_methods + default_find_methods
diff --git a/atest/test_finder_handler_unittest.py b/atest/test_finder_handler_unittest.py
new file mode 100755
index 0000000..8f5e822
--- /dev/null
+++ b/atest/test_finder_handler_unittest.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for test_finder_handler."""
+
+import unittest
+import mock
+
+import atest_error
+import test_finder_handler
+from test_finders import test_info
+from test_finders import test_finder_base
+
+#pylint: disable=protected-access
+REF_TYPE = test_finder_handler._REFERENCE_TYPE
+
+_EXAMPLE_FINDER_A = 'EXAMPLE_A'
+
+
+#pylint: disable=no-self-use
+@test_finder_base.find_method_register
+class ExampleFinderA(test_finder_base.TestFinderBase):
+    """Example finder class A."""
+    NAME = _EXAMPLE_FINDER_A
+    _TEST_RUNNER = 'TEST_RUNNER'
+
+    @test_finder_base.register()
+    def registered_find_method_from_example_finder(self, test):
+        """Registered Example find method."""
+        if test == 'ExampleFinderATrigger':
+            return test_info.TestInfo(test_name=test,
+                                      test_runner=self._TEST_RUNNER,
+                                      build_targets=set())
+        return None
+
+    def unregistered_find_method_from_example_finder(self, _test):
+        """Unregistered Example find method, should never be called."""
+        raise atest_error.ShouldNeverBeCalledError()
+
+
+_TEST_FINDERS_PATCH = {
+    ExampleFinderA,
+}
+
+
+_FINDER_INSTANCES = {
+    _EXAMPLE_FINDER_A: ExampleFinderA(),
+}
+
+
+class TestFinderHandlerUnittests(unittest.TestCase):
+    """Unit tests for test_finder_handler.py"""
+
+    def setUp(self):
+        """Set up for testing."""
+        # pylint: disable=invalid-name
+        # This is so we can see the full diffs when there are mismatches.
+        self.maxDiff = None
+        self.empty_mod_info = None
+        # We want to control the finders we return.
+        mock.patch('test_finder_handler._get_test_finders',
+                   lambda: _TEST_FINDERS_PATCH).start()
+        # Since we're going to be comparing instance objects, we'll need to keep
+        # track of the objects so they align.
+        mock.patch('test_finder_handler._get_finder_instance_dict',
+                   lambda x: _FINDER_INSTANCES).start()
+        # We want to mock out the default find methods to make sure we got all
+        # the methods we expect.
+        mock.patch('test_finder_handler._get_default_find_methods',
+                   lambda x, y: [test_finder_base.Finder(
+                       _FINDER_INSTANCES[_EXAMPLE_FINDER_A],
+                       ExampleFinderA.unregistered_find_method_from_example_finder,
+                       _EXAMPLE_FINDER_A)]).start()
+
+    def tearDown(self):
+        """Tear down."""
+        mock.patch.stopall()
+
+    def test_get_test_reference_types(self):
+        """Test _get_test_reference_types parses reference types correctly."""
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('ModuleOrClassName'),
+            [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.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.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.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.CACHE, REF_TYPE.MODULE, REF_TYPE.PACKAGE]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('fully.q.Class'),
+            [REF_TYPE.CACHE, REF_TYPE.QUALIFIED_CLASS]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('Integration.xml'),
+            [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.CACHE, REF_TYPE.MODULE_FILE_PATH]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('SomeClass.kt'),
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('Android.mk'),
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('Android.bp'),
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('SomeTest.cc'),
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('SomeTest.cpp'),
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('SomeTest.cc#method'),
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_FILE_PATH]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('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.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('module:a.package'),
+            [REF_TYPE.CACHE, REF_TYPE.MODULE_PACKAGE]
+        )
+        self.assertEqual(
+            test_finder_handler._get_test_reference_types('.'),
+            [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.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.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.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.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.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.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.CACHE, REF_TYPE.INTEGRATION, REF_TYPE.MODULE_CLASS]
+        )
+
+    def test_get_registered_find_methods(self):
+        """Test that we get the registered find methods."""
+        empty_mod_info = None
+        example_finder_a_instance = test_finder_handler._get_finder_instance_dict(
+            empty_mod_info)[_EXAMPLE_FINDER_A]
+        should_equal = [
+            test_finder_base.Finder(
+                example_finder_a_instance,
+                ExampleFinderA.registered_find_method_from_example_finder,
+                _EXAMPLE_FINDER_A)]
+        should_not_equal = [
+            test_finder_base.Finder(
+                example_finder_a_instance,
+                ExampleFinderA.unregistered_find_method_from_example_finder,
+                _EXAMPLE_FINDER_A)]
+        # Let's make sure we see the registered method.
+        self.assertEqual(
+            should_equal,
+            test_finder_handler._get_registered_find_methods(empty_mod_info)
+        )
+        # Make sure we don't see the unregistered method here.
+        self.assertNotEqual(
+            should_not_equal,
+            test_finder_handler._get_registered_find_methods(empty_mod_info)
+        )
+
+    def test_get_find_methods_for_test(self):
+        """Test that we get the find methods we expect."""
+        # Let's see that we get the unregistered and registered find methods in
+        # the order we expect.
+        test = ''
+        registered_find_methods = [
+            test_finder_base.Finder(
+                _FINDER_INSTANCES[_EXAMPLE_FINDER_A],
+                ExampleFinderA.registered_find_method_from_example_finder,
+                _EXAMPLE_FINDER_A)]
+        default_find_methods = [
+            test_finder_base.Finder(
+                _FINDER_INSTANCES[_EXAMPLE_FINDER_A],
+                ExampleFinderA.unregistered_find_method_from_example_finder,
+                _EXAMPLE_FINDER_A)]
+        should_equal = registered_find_methods + default_find_methods
+        self.assertEqual(
+            should_equal,
+            test_finder_handler.get_find_methods_for_test(self.empty_mod_info,
+                                                          test))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_finders/__init__.py b/atest/test_finders/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/test_finders/__init__.py
diff --git a/atest/test_finders/cache_finder.py b/atest/test_finders/cache_finder.py
new file mode 100644
index 0000000..5b7bd07
--- /dev/null
+++ b/atest/test_finders/cache_finder.py
@@ -0,0 +1,61 @@
+# 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
+from test_finders import test_info
+
+class CacheFinder(test_finder_base.TestFinderBase):
+    """Cache Finder class."""
+    NAME = 'CACHE'
+
+    def __init__(self, **kwargs):
+        super(CacheFinder, self).__init__()
+
+    def _is_latest_testinfos(self, test_infos):
+        """Check whether test_infos are up-to-date.
+
+        Args:
+            test_infos: A list of TestInfo.
+
+        Returns:
+            True if all keys in test_infos and TestInfo object are equal.
+            Otherwise, False.
+        """
+        sorted_base_ti = sorted(
+            vars(test_info.TestInfo(None, None, None)).keys())
+        for cached_test_info in test_infos:
+            sorted_cache_ti = sorted(vars(cached_test_info).keys())
+            if not sorted_cache_ti == sorted_base_ti:
+                return False
+        return True
+
+    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 and is in latest
+            TestInfo format, else None.
+        """
+        test_infos = atest_utils.load_test_info_cache(test_reference)
+        if test_infos and self._is_latest_testinfos(test_infos):
+            return test_infos
+        return None
diff --git a/atest/test_finders/cache_finder_unittest.py b/atest/test_finders/cache_finder_unittest.py
new file mode 100755
index 0000000..7797ea3
--- /dev/null
+++ b/atest/test_finders/cache_finder_unittest.py
@@ -0,0 +1,62 @@
+#!/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."""
+        uncached_test = 'mytest1'
+        cached_test = 'hello_world_test'
+        uncached_test2 = 'mytest2'
+        test_cache_root = os.path.join(uc.TEST_DATA_DIR, 'cache_root')
+        # Hit matched cache file but no original_finder in it,
+        # should return None.
+        mock_get_cache_path.return_value = os.path.join(
+            test_cache_root,
+            'cd66f9f5ad63b42d0d77a9334de6bb73.cache')
+        self.assertIsNone(self.cache_finder.find_test_by_cache(uncached_test))
+        # Hit matched cache file and original_finder is in it,
+        # should return cached test infos.
+        mock_get_cache_path.return_value = os.path.join(
+            test_cache_root,
+            '78ea54ef315f5613f7c11dd1a87f10c7.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_test2))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_finders/example_finder.py b/atest/test_finders/example_finder.py
new file mode 100644
index 0000000..d1fc33b
--- /dev/null
+++ b/atest/test_finders/example_finder.py
@@ -0,0 +1,38 @@
+# Copyright 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.
+
+"""
+Example Finder class.
+"""
+
+# pylint: disable=import-error
+from test_finders import test_info
+from test_finders import test_finder_base
+from test_runners import example_test_runner
+
+
+@test_finder_base.find_method_register
+class ExampleFinder(test_finder_base.TestFinderBase):
+    """Example finder class."""
+    NAME = 'EXAMPLE'
+    _TEST_RUNNER = example_test_runner.ExampleTestRunner.NAME
+
+    @test_finder_base.register()
+    def find_method_from_example_finder(self, test):
+        """Example find method to demonstrate how to register it."""
+        if test == 'ExampleFinderTest':
+            return test_info.TestInfo(test_name=test,
+                                      test_runner=self._TEST_RUNNER,
+                                      build_targets=set())
+        return None
diff --git a/atest/test_finders/module_finder.py b/atest/test_finders/module_finder.py
new file mode 100644
index 0000000..ac9fdb2
--- /dev/null
+++ b/atest/test_finders/module_finder.py
@@ -0,0 +1,609 @@
+# Copyright 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.
+
+"""
+Module Finder class.
+"""
+
+import logging
+import os
+
+# pylint: disable=import-error
+import atest_error
+import atest_utils
+import constants
+from test_finders import test_info
+from test_finders import test_finder_base
+from test_finders import test_finder_utils
+from test_runners import atest_tf_test_runner
+from test_runners import robolectric_test_runner
+from test_runners import vts_tf_test_runner
+
+_MODULES_IN = 'MODULES-IN-%s'
+_ANDROID_MK = 'Android.mk'
+
+# These are suites in LOCAL_COMPATIBILITY_SUITE that aren't really suites so
+# we can ignore them.
+_SUITES_TO_IGNORE = frozenset({'general-tests', 'device-tests', 'tests'})
+
+class ModuleFinder(test_finder_base.TestFinderBase):
+    """Module finder class."""
+    NAME = 'MODULE'
+    _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME
+    _ROBOLECTRIC_RUNNER = robolectric_test_runner.RobolectricTestRunner.NAME
+    _VTS_TEST_RUNNER = vts_tf_test_runner.VtsTradefedTestRunner.NAME
+
+    def __init__(self, module_info=None):
+        super(ModuleFinder, self).__init__()
+        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
+        self.module_info = module_info
+
+    def _determine_testable_module(self, path):
+        """Determine which module the user is trying to test.
+
+        Returns the module to test. If there are multiple possibilities, will
+        ask the user. Otherwise will return the only module found.
+
+        Args:
+            path: String path of module to look for.
+
+        Returns:
+            A list of the module names.
+        """
+        testable_modules = []
+        for mod in self.module_info.get_module_names(path):
+            mod_info = self.module_info.get_module_info(mod)
+            # Robolectric tests always exist in pairs of 2, one module to build
+            # 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 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)
+
+    def _is_vts_module(self, module_name):
+        """Returns True if the module is a vts module, else False."""
+        mod_info = self.module_info.get_module_info(module_name)
+        suites = []
+        if mod_info:
+            suites = mod_info.get('compatibility_suites', [])
+        # Pull out all *ts (cts, tvts, etc) suites.
+        suites = [suite for suite in suites if suite not in _SUITES_TO_IGNORE]
+        return len(suites) == 1 and 'vts' in suites
+
+    def _update_to_vts_test_info(self, test):
+        """Fill in the fields with vts specific info.
+
+        We need to update the runner to use the vts runner and also find the
+        test specific dependencies.
+
+        Args:
+            test: TestInfo to update with vts specific details.
+
+        Return:
+            TestInfo that is ready for the vts test runner.
+        """
+        test.test_runner = self._VTS_TEST_RUNNER
+        config_file = os.path.join(self.root_dir,
+                                   test.data[constants.TI_REL_CONFIG])
+        # Need to get out dir (special logic is to account for custom out dirs).
+        # The out dir is used to construct the build targets for the test deps.
+        out_dir = os.environ.get(constants.ANDROID_HOST_OUT)
+        custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR)
+        # If we're not an absolute custom out dir, get relative out dir path.
+        if custom_out_dir is None or not os.path.isabs(custom_out_dir):
+            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_paths = test_finder_utils.search_integration_dirs(
+            constants.VTS_STAGING_PLAN,
+            self.module_info.get_paths(constants.VTS_TF_MODULE))
+        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(
+                config_file, vts_out_dir, self.module_info)
+        test.build_targets.add('vts-test-core')
+        test.build_targets.add(test.test_name)
+        return test
+
+    def _update_to_robolectric_test_info(self, test):
+        """Update the fields for a robolectric test.
+
+        Args:
+          test: TestInfo to be updated with robolectric fields.
+
+        Returns:
+          TestInfo with robolectric fields.
+        """
+        test.test_runner = self._ROBOLECTRIC_RUNNER
+        test.test_name = self.module_info.get_robolectric_test_name(test.test_name)
+        return test
+
+    def _process_test_info(self, test):
+        """Process the test info and return some fields updated/changed.
+
+        We need to check if the test found is a special module (like vts) and
+        update the test_info fields (like test_runner) appropriately.
+
+        Args:
+            test: TestInfo that has been filled out by a find method.
+
+        Return:
+            TestInfo that has been modified as needed and return None if
+            this module can't be found in the module_info.
+        """
+        module_name = test.test_name
+        mod_info = self.module_info.get_module_info(module_name)
+        if not mod_info:
+            return None
+        test.module_class = mod_info['class']
+        test.install_locations = test_finder_utils.get_install_locations(
+            mod_info['installed'])
+        # Check if this is only a vts module.
+        if self._is_vts_module(test.test_name):
+            return self._update_to_vts_test_info(test)
+        elif self.module_info.is_robolectric_test(test.test_name):
+            return self._update_to_robolectric_test_info(test)
+        rel_config = test.data[constants.TI_REL_CONFIG]
+        test.build_targets = self._get_build_targets(module_name, rel_config)
+        return test
+
+    def _get_build_targets(self, module_name, rel_config):
+        """Get the test deps.
+
+        Args:
+            module_name: name of the test.
+            rel_config: XML for the given test.
+
+        Returns:
+            Set of build targets.
+        """
+        targets = set()
+        if not self.module_info.is_auto_gen_test_config(module_name):
+            config_file = os.path.join(self.root_dir, rel_config)
+            targets = test_finder_utils.get_targets_from_xml(config_file,
+                                                             self.module_info)
+        for module_path in self.module_info.get_paths(module_name):
+            mod_dir = module_path.replace('/', '-')
+            targets.add(_MODULES_IN % mod_dir)
+        return targets
+
+    def _get_module_test_config(self, module_name, rel_config=None):
+        """Get the value of test_config in module_info.
+
+        Get the value of 'test_config' in module_info if its
+        auto_test_config is not true.
+        In this case, the test_config is specified by user.
+        If not, return rel_config.
+
+        Args:
+            module_name: A string of the test's module name.
+            rel_config: XML for the given test.
+
+        Returns:
+            A string of test_config path if found, else return rel_config.
+        """
+        mod_info = self.module_info.get_module_info(module_name)
+        if mod_info:
+            test_config = ''
+            test_config_list = mod_info.get(constants.MODULE_TEST_CONFIG, [])
+            if test_config_list:
+                test_config = test_config_list[0]
+            if not self.module_info.is_auto_gen_test_config(module_name) and test_config != '':
+                return test_config
+        return rel_config
+
+    def _get_test_info_filter(self, path, methods, **kwargs):
+        """Get test info filter.
+
+        Args:
+            path: A string of the test's path.
+            methods: A set of method name strings.
+            rel_module_dir: Optional. A string of the module dir relative to
+                root.
+            class_name: Optional. A string of the class name.
+            is_native_test: Optional. A boolean variable of whether to search
+                for a native test or not.
+
+        Returns:
+            A set of test info filter.
+        """
+        _, file_name = test_finder_utils.get_dir_path_and_filename(path)
+        ti_filter = frozenset()
+        if kwargs.get('is_native_test', None):
+            ti_filter = frozenset([test_info.TestFilter(
+                test_finder_utils.get_cc_filter(
+                    kwargs.get('class_name', '*'), methods), frozenset())])
+        # Path to java file.
+        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 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)
+            if methods:
+                ti_filter = frozenset(
+                    [test_info.TestFilter(test_finder_utils.get_cc_filter(
+                        kwargs.get('class_name', '*'), methods), frozenset())])
+        # Path to non-module dir, treat as package.
+        elif (not file_name
+              and kwargs.get('rel_module_dir', None) !=
+              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 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.
+                        if methods:
+                            raise atest_error.MethodWithoutClassError(
+                                '%s: Method filtering requires class'
+                                % str(methods))
+                        ti_filter = frozenset(
+                            [test_info.TestFilter(package_name, methods)])
+                        break
+        return ti_filter
+
+    def _get_rel_config(self, test_path):
+        """Get config file's relative path.
+
+        Args:
+            test_path: A string of the test absolute path.
+
+        Returns:
+            A string of config's relative path, else None.
+        """
+        test_dir = os.path.dirname(test_path)
+        rel_module_dir = test_finder_utils.find_parent_module_dir(
+            self.root_dir, test_dir, self.module_info)
+        if rel_module_dir:
+            return os.path.join(rel_module_dir, constants.MODULE_CONFIG)
+        return None
+
+    def _get_test_infos(self, test_path, rel_config, module_name, test_filter):
+        """Get test_info for test_path.
+
+        Args:
+            test_path: A string of the test path.
+            rel_config: A string of rel path of config.
+            module_name: A string of the module name to use.
+            test_filter: A test info filter.
+
+        Returns:
+            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 module_name:
+            module_names = [module_name]
+        else:
+            module_names = self._determine_testable_module(
+                os.path.dirname(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)
+                mod_info = self.module_info.get_module_info(mname)
+                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},
+                    compatibility_suites=mod_info.get(
+                        constants.MODULE_COMPATIBILITY_SUITES, [])))
+                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.
+
+        Args:
+            module_name: A string of the test's module name.
+
+        Returns:
+            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):
+            # path is a list with only 1 element.
+            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)
+            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()},
+                compatibility_suites=mod_info.get(
+                    constants.MODULE_COMPATIBILITY_SUITES, [])))
+            if tinfo:
+                return [tinfo]
+        return None
+
+    def find_test_by_class_name(self, class_name, module_name=None,
+                                rel_config=None, is_native_test=False):
+        """Find test files given a class name.
+
+        If module_name and rel_config not given it will calculate it determine
+        it by looking up the tree from the class file.
+
+        Args:
+            class_name: A string of the test's class name.
+            module_name: Optional. A string of the module name to use.
+            rel_config: Optional. A string of module dir relative to repo root.
+            is_native_test: A boolean variable of whether to search for a
+            native test or not.
+
+        Returns:
+            A list of populated TestInfo namedtuple if test found, else None.
+        """
+        class_name, methods = test_finder_utils.split_methods(class_name)
+        if rel_config:
+            search_dir = os.path.join(self.root_dir,
+                                      os.path.dirname(rel_config))
+        else:
+            search_dir = self.root_dir
+        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_paths = test_finder_utils.find_class_file(self.root_dir,
+                                                           class_name,
+                                                           is_native_test,
+                                                           methods)
+        if not test_paths:
+            return None
+        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.
+
+        Args:
+            module_class: A string of form MODULE:CLASS or MODULE:CLASS#METHOD.
+
+        Returns:
+            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_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.
+        find_result = None
+        if not self.module_info.is_native_test(module_name):
+            # Find by java class.
+            find_result = self.find_test_by_class_name(
+                class_name, module_info.test_name,
+                module_info.data.get(constants.TI_REL_CONFIG))
+        # Find by cc class.
+        if not find_result:
+            find_result = self.find_test_by_cc_class_name(
+                class_name, module_info.test_name,
+                module_info.data.get(constants.TI_REL_CONFIG))
+        return find_result
+
+    def find_test_by_package_name(self, package, module_name=None,
+                                  rel_config=None):
+        """Find the test info given a PACKAGE string.
+
+        Args:
+            package: A string of the package name.
+            module_name: Optional. A string of the module name.
+            ref_config: Optional. A string of rel path of config.
+
+        Returns:
+            A list of populated TestInfo namedtuple if found, else None.
+        """
+        _, methods = test_finder_utils.split_methods(package)
+        if methods:
+            raise atest_error.MethodWithoutClassError('%s: Method filtering '
+                                                      'requires class' % (
+                                                          methods))
+        # Confirm that packages exists and get user input for multiples.
+        if rel_config:
+            search_dir = os.path.join(self.root_dir,
+                                      os.path.dirname(rel_config))
+        else:
+            search_dir = self.root_dir
+        package_paths = test_finder_utils.run_find_cmd(
+            test_finder_utils.FIND_REFERENCE_TYPE.PACKAGE, search_dir, package)
+        # Package path will be the full path to the dir represented by package.
+        if not package_paths:
+            return None
+        test_filter = frozenset([test_info.TestFilter(package, frozenset())])
+        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.
+
+        Args:
+            module_package: A string of form MODULE:PACKAGE
+
+        Returns:
+            A list of populated TestInfo namedtuple if found, else None.
+        """
+        module_name, package = module_package.split(':')
+        # 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(
+            package, module_info.test_name,
+            module_info.data.get(constants.TI_REL_CONFIG))
+
+    def find_test_by_path(self, path):
+        """Find the first test info matching the given path.
+
+        Strategy:
+            path_to_java_file --> Resolve to CLASS
+            path_to_cc_file --> Resolve to CC CLASS
+            path_to_module_file -> Resolve to MODULE
+            path_to_module_dir -> Resolve to MODULE
+            path_to_dir_with_class_files--> Resolve to PACKAGE
+            path_to_any_other_dir --> Resolve as MODULE
+
+        Args:
+            path: A string of the test's path.
+
+        Returns:
+            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)
+        # TODO: See if this can be generalized and shared with methods above
+        # create absolute path from cwd and remove symbolic links
+        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(
+            self.root_dir, dir_path, self.module_info)
+        if not rel_module_dir:
+            return None
+        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_infos(path, rel_config, None, test_filter)
+
+    def find_test_by_cc_class_name(self, class_name, module_name=None,
+                                   rel_config=None):
+        """Find test files given a cc class name.
+
+        If module_name and rel_config not given, test will be determined
+        by looking up the tree for files which has input class.
+
+        Args:
+            class_name: A string of the test's class name.
+            module_name: Optional. A string of the module name to use.
+            rel_config: Optional. A string of module dir relative to repo root.
+
+        Returns:
+            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.
+        if '.' in class_name:
+            # Assume the class name has a format of file_name.class_name
+            class_name = class_name[class_name.rindex('.')+1:]
+            logging.info('Search with updated class name: %s', class_name)
+        return self.find_test_by_class_name(
+            class_name, module_name, rel_config, is_native_test=True)
+
+    def get_testable_modules_with_ld(self, user_input, ld_range=0):
+        """Calculate the edit distances of the input and testable modules.
+
+        The user input will be calculated across all testable modules and
+        results in integers generated by Levenshtein Distance algorithm.
+        To increase the speed of the calculation, a bound can be applied to
+        this method to prevent from calculating every testable modules.
+
+        Guessing from typos, e.g. atest atest_unitests, implies a tangible range
+        of length that Atest only needs to search within it, and the default of
+        the bound is 2.
+
+        Guessing from keywords however, e.g. atest --search Camera, means that
+        the uncertainty of the module name is way higher, and Atest should walk
+        through all testable modules and return the highest possibilities.
+
+        Args:
+            user_input: A string of the user input.
+            ld_range: An integer that range the searching scope. If the length of
+                      user_input is 10, then Atest will calculate modules of which
+                      length is between 8 and 12. 0 is equivalent to unlimited.
+
+        Returns:
+            A List of LDs and possible module names. If the user_input is "fax",
+            the output will be like:
+            [[2, "fog"], [2, "Fix"], [4, "duck"], [7, "Duckies"]]
+
+            Which means the most lilely names of "fax" are fog and Fix(LD=2),
+            while Dickies is the most unlikely one(LD=7).
+        """
+        atest_utils.colorful_print('\nSearching for similar module names using '
+                                   'fuzzy search...', constants.CYAN)
+        testable_modules = sorted(self.module_info.get_testable_modules(), key=len)
+        lower_bound = len(user_input) - ld_range
+        upper_bound = len(user_input) + ld_range
+        testable_modules_with_ld = []
+        for module_name in testable_modules:
+            # Dispose those too short or too lengthy.
+            if ld_range != 0:
+                if len(module_name) < lower_bound:
+                    continue
+                elif len(module_name) > upper_bound:
+                    break
+            testable_modules_with_ld.append(
+                [test_finder_utils.get_levenshtein_distance(
+                    user_input, module_name), module_name])
+        return testable_modules_with_ld
+
+    def get_fuzzy_searching_results(self, user_input):
+        """Give results which have no more than allowance of edit distances.
+
+        Args:
+            user_input: the target module name for fuzzy searching.
+
+        Return:
+            A list of guessed modules.
+        """
+        modules_with_ld = self.get_testable_modules_with_ld(user_input,
+                                                            ld_range=constants.LD_RANGE)
+        guessed_modules = []
+        for _distance, _module in modules_with_ld:
+            if _distance <= abs(constants.LD_RANGE):
+                guessed_modules.append(_module)
+        return guessed_modules
diff --git a/atest/test_finders/module_finder_unittest.py b/atest/test_finders/module_finder_unittest.py
new file mode 100755
index 0000000..bba9434
--- /dev/null
+++ b/atest/test_finders/module_finder_unittest.py
@@ -0,0 +1,542 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for module_finder."""
+
+import re
+import unittest
+import os
+import mock
+
+# pylint: disable=import-error
+import atest_error
+import constants
+import module_info
+import unittest_constants as uc
+import unittest_utils
+from test_finders import module_finder
+from test_finders import test_finder_utils
+from test_finders import test_info
+from test_runners import atest_tf_test_runner as atf_tr
+
+MODULE_CLASS = '%s:%s' % (uc.MODULE_NAME, uc.CLASS_NAME)
+MODULE_PACKAGE = '%s:%s' % (uc.MODULE_NAME, uc.PACKAGE)
+CC_MODULE_CLASS = '%s:%s' % (uc.CC_MODULE_NAME, uc.CC_CLASS_NAME)
+FLAT_METHOD_INFO = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    uc.MODULE_BUILD_TARGETS,
+    data={constants.TI_FILTER: frozenset([uc.FLAT_METHOD_FILTER]),
+          constants.TI_REL_CONFIG: uc.CONFIG_FILE})
+MODULE_CLASS_METHOD = '%s#%s' % (MODULE_CLASS, uc.METHOD_NAME)
+CC_MODULE_CLASS_METHOD = '%s#%s' % (CC_MODULE_CLASS, uc.CC_METHOD_NAME)
+CLASS_INFO_MODULE_2 = test_info.TestInfo(
+    uc.MODULE2_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    uc.CLASS_BUILD_TARGETS,
+    data={constants.TI_FILTER: frozenset([uc.CLASS_FILTER]),
+          constants.TI_REL_CONFIG: uc.CONFIG2_FILE})
+CC_CLASS_INFO_MODULE_2 = test_info.TestInfo(
+    uc.CC_MODULE2_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    uc.CLASS_BUILD_TARGETS,
+    data={constants.TI_FILTER: frozenset([uc.CC_CLASS_FILTER]),
+          constants.TI_REL_CONFIG: uc.CC_CONFIG2_FILE})
+DEFAULT_INSTALL_PATH = ['/path/to/install']
+ROBO_MOD_PATH = ['/shared/robo/path']
+NON_RUN_ROBO_MOD_NAME = 'robo_mod'
+RUN_ROBO_MOD_NAME = 'run_robo_mod'
+NON_RUN_ROBO_MOD = {constants.MODULE_NAME: NON_RUN_ROBO_MOD_NAME,
+                    constants.MODULE_PATH: ROBO_MOD_PATH,
+                    constants.MODULE_CLASS: ['random_class']}
+RUN_ROBO_MOD = {constants.MODULE_NAME: RUN_ROBO_MOD_NAME,
+                constants.MODULE_PATH: ROBO_MOD_PATH,
+                constants.MODULE_CLASS: [constants.MODULE_CLASS_ROBOLECTRIC]}
+
+SEARCH_DIR_RE = re.compile(r'^find ([^ ]*).*$')
+
+#pylint: disable=unused-argument
+def classoutside_side_effect(find_cmd, shell=False):
+    """Mock the check output of a find cmd where class outside module path."""
+    search_dir = SEARCH_DIR_RE.match(find_cmd).group(1).strip()
+    if search_dir == uc.ROOT:
+        return uc.FIND_ONE
+    return None
+
+
+#pylint: disable=protected-access
+class ModuleFinderUnittests(unittest.TestCase):
+    """Unit tests for module_finder.py"""
+
+    def setUp(self):
+        """Set up stuff for testing."""
+        self.mod_finder = module_finder.ModuleFinder()
+        self.mod_finder.module_info = mock.Mock(spec=module_info.ModuleInfo)
+        self.mod_finder.module_info.path_to_module_info = {}
+        self.mod_finder.root_dir = uc.ROOT
+
+    def test_is_vts_module(self):
+        """Test _load_module_info_file regular operation."""
+        mod_name = 'mod'
+        is_vts_module_info = {'compatibility_suites': ['vts', 'tests']}
+        self.mod_finder.module_info.get_module_info.return_value = is_vts_module_info
+        self.assertTrue(self.mod_finder._is_vts_module(mod_name))
+
+        is_not_vts_module = {'compatibility_suites': ['vts', 'cts']}
+        self.mod_finder.module_info.get_module_info.return_value = is_not_vts_module
+        self.assertFalse(self.mod_finder._is_vts_module(mod_name))
+
+    # pylint: disable=unused-argument
+    @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets',
+                       return_value=uc.MODULE_BUILD_TARGETS)
+    def test_find_test_by_module_name(self, _get_targ):
+        """Test find_test_by_module_name."""
+        self.mod_finder.module_info.is_robolectric_test.return_value = False
+        self.mod_finder.module_info.has_test_config.return_value = True
+        mod_info = {'installed': ['/path/to/install'],
+                    'path': [uc.MODULE_DIR],
+                    constants.MODULE_CLASS: [],
+                    constants.MODULE_COMPATIBILITY_SUITES: []}
+        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,
+            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')
+    @mock.patch('subprocess.check_output', return_value=uc.FIND_ONE)
+    @mock.patch.object(test_finder_utils, 'get_fully_qualified_class_name',
+                       return_value=uc.FULL_CLASS_NAME)
+    @mock.patch('os.path.isfile', side_effect=unittest_utils.isfile_side_effect)
+    @mock.patch('os.path.isdir', return_value=True)
+    #pylint: disable=unused-argument
+    def test_find_test_by_class_name(self, _isdir, _isfile, _fqcn,
+                                     mock_checkoutput, mock_build,
+                                     _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
+        self.mod_finder.module_info.is_robolectric_test.return_value = False
+        self.mod_finder.module_info.has_test_config.return_value = True
+        self.mod_finder.module_info.get_module_names.return_value = [uc.MODULE_NAME]
+        self.mod_finder.module_info.get_module_info.return_value = {
+            constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+            constants.MODULE_NAME: uc.MODULE_NAME,
+            constants.MODULE_CLASS: [],
+            constants.MODULE_COMPATIBILITY_SUITES: []}
+        t_infos = self.mod_finder.find_test_by_class_name(uc.CLASS_NAME)
+        unittest_utils.assert_equal_testinfos(
+            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, 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, 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, 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
+        t_infos = self.mod_finder.find_test_by_class_name(uc.CLASS_NAME,
+                                                          uc.MODULE2_NAME,
+                                                          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')
+    @mock.patch('subprocess.check_output', return_value=uc.FIND_ONE)
+    @mock.patch.object(test_finder_utils, 'get_fully_qualified_class_name',
+                       return_value=uc.FULL_CLASS_NAME)
+    @mock.patch('os.path.isfile', side_effect=unittest_utils.isfile_side_effect)
+    #pylint: disable=unused-argument
+    def test_find_test_by_module_and_class(self, _isfile, _fqcn,
+                                           mock_checkoutput, mock_build,
+                                           _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
+        self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
+        self.mod_finder.module_info.is_robolectric_test.return_value = False
+        self.mod_finder.module_info.has_test_config.return_value = True
+        mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        mod_info = {constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+                    constants.MODULE_PATH: [uc.MODULE_DIR],
+                    constants.MODULE_CLASS: [],
+                    constants.MODULE_COMPATIBILITY_SUITES: []}
+        self.mod_finder.module_info.get_module_info.return_value = mod_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_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)
+        self.mod_finder.module_info.get_module_info.return_value = None
+        self.assertIsNone(self.mod_finder.find_test_by_module_and_class(bad_module))
+        # find output fails to find class file
+        mock_checkoutput.return_value = ''
+        bad_class = '%s:%s' % (uc.MODULE_NAME, 'Anything')
+        self.mod_finder.module_info.get_module_info.return_value = mod_info
+        self.assertIsNone(self.mod_finder.find_test_by_module_and_class(bad_class))
+
+    @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
+                       return_value=False)
+    @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
+    @mock.patch('subprocess.check_output', return_value=uc.FIND_CC_ONE)
+    @mock.patch.object(test_finder_utils, 'find_class_file',
+                       side_effect=[None, None, '/'])
+    @mock.patch('os.path.isfile', side_effect=unittest_utils.isfile_side_effect)
+    #pylint: disable=unused-argument
+    def test_find_test_by_module_and_class_part_2(self, _isfile, mock_fcf,
+                                                  mock_checkoutput, mock_build,
+                                                  _vts):
+        """Test find_test_by_module_and_class for MODULE:CC_CLASS."""
+        # Native test was tested in test_find_test_by_cc_class_name()
+        self.mod_finder.module_info.is_native_test.return_value = False
+        self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
+        self.mod_finder.module_info.is_robolectric_test.return_value = False
+        self.mod_finder.module_info.has_test_config.return_value = True
+        mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        mod_info = {constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+                    constants.MODULE_PATH: [uc.CC_MODULE_DIR],
+                    constants.MODULE_CLASS: [],
+                    constants.MODULE_COMPATIBILITY_SUITES: []}
+        self.mod_finder.module_info.get_module_info.return_value = mod_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_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
+        self.mod_finder.module_info.is_testable_module.return_value = False
+        self.assertIsNone(self.mod_finder.find_test_by_module_and_class(bad_module))
+
+    @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
+                       return_value=False)
+    @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
+    @mock.patch('subprocess.check_output', return_value=uc.FIND_PKG)
+    @mock.patch('os.path.isfile', side_effect=unittest_utils.isfile_side_effect)
+    @mock.patch('os.path.isdir', return_value=True)
+    #pylint: disable=unused-argument
+    def test_find_test_by_package_name(self, _isdir, _isfile, mock_checkoutput,
+                                       mock_build, _vts):
+        """Test find_test_by_package_name."""
+        self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
+        self.mod_finder.module_info.is_robolectric_test.return_value = False
+        self.mod_finder.module_info.has_test_config.return_value = True
+        mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        self.mod_finder.module_info.get_module_names.return_value = [uc.MODULE_NAME]
+        self.mod_finder.module_info.get_module_info.return_value = {
+            constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+            constants.MODULE_NAME: uc.MODULE_NAME,
+            constants.MODULE_CLASS: [],
+            constants.MODULE_COMPATIBILITY_SUITES: []
+            }
+        t_infos = self.mod_finder.find_test_by_package_name(uc.PACKAGE)
+        unittest_utils.assert_equal_testinfos(
+            self, t_infos[0],
+            uc.PACKAGE_INFO)
+        # with method, should raise
+        pkg_with_method = '%s#%s' % (uc.PACKAGE, uc.METHOD_NAME)
+        self.assertRaises(atest_error.MethodWithoutClassError,
+                          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, 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'))
+
+    @mock.patch('os.path.isdir', return_value=False)
+    @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
+                       return_value=False)
+    @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets')
+    @mock.patch('subprocess.check_output', return_value=uc.FIND_PKG)
+    @mock.patch('os.path.isfile', side_effect=unittest_utils.isfile_side_effect)
+    #pylint: disable=unused-argument
+    def test_find_test_by_module_and_package(self, _isfile, mock_checkoutput,
+                                             mock_build, _vts, _isdir):
+        """Test find_test_by_module_and_package."""
+        self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
+        self.mod_finder.module_info.is_robolectric_test.return_value = False
+        self.mod_finder.module_info.has_test_config.return_value = True
+        mock_build.return_value = uc.CLASS_BUILD_TARGETS
+        mod_info = {constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+                    constants.MODULE_PATH: [uc.MODULE_DIR],
+                    constants.MODULE_CLASS: [],
+                    constants.MODULE_COMPATIBILITY_SUITES: []}
+        self.mod_finder.module_info.get_module_info.return_value = mod_info
+        t_infos = self.mod_finder.find_test_by_module_and_package(MODULE_PACKAGE)
+        self.assertEqual(t_infos, None)
+        _isdir.return_value = True
+        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)
+        self.assertRaises(atest_error.MethodWithoutClassError,
+                          self.mod_finder.find_test_by_module_and_package,
+                          module_pkg_with_method)
+        # bad module, good pkg, returns None
+        self.mod_finder.module_info.is_testable_module.return_value = False
+        bad_module = '%s:%s' % ('BadMod', uc.PACKAGE)
+        self.mod_finder.module_info.get_module_info.return_value = None
+        self.assertIsNone(self.mod_finder.find_test_by_module_and_package(bad_module))
+        # find output fails to find package path
+        mock_checkoutput.return_value = ''
+        bad_pkg = '%s:%s' % (uc.MODULE_NAME, 'Anything')
+        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')
+    @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
+                       return_value=False)
+    @mock.patch.object(test_finder_utils, 'get_fully_qualified_class_name',
+                       return_value=uc.FULL_CLASS_NAME)
+    @mock.patch('os.path.realpath',
+                side_effect=unittest_utils.realpath_side_effect)
+    @mock.patch('os.path.isfile', side_effect=unittest_utils.isfile_side_effect)
+    @mock.patch.object(test_finder_utils, 'find_parent_module_dir')
+    @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,
+                               _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
+        mock_build.return_value = set()
+        # Check that we don't return anything with invalid test references.
+        mock_pathexists.return_value = False
+        unittest_utils.assert_equal_testinfos(
+            self, None, self.mod_finder.find_test_by_path('bad/path'))
+        mock_pathexists.return_value = True
+        mock_dir.return_value = None
+        unittest_utils.assert_equal_testinfos(
+            self, None, self.mod_finder.find_test_by_path('no/module'))
+        self.mod_finder.module_info.get_module_names.return_value = [uc.MODULE_NAME]
+        self.mod_finder.module_info.get_module_info.return_value = {
+            constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+            constants.MODULE_NAME: uc.MODULE_NAME,
+            constants.MODULE_CLASS: [],
+            constants.MODULE_COMPATIBILITY_SUITES: []}
+
+        # Happy path testing.
+        mock_dir.return_value = uc.MODULE_DIR
+
+        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, 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, 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, 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, t_infos[0],
+            FLAT_METHOD_INFO)
+
+        # Cc path testing.
+        self.mod_finder.module_info.get_module_names.return_value = [uc.CC_MODULE_NAME]
+        self.mod_finder.module_info.get_module_info.return_value = {
+            constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+            constants.MODULE_NAME: uc.CC_MODULE_NAME,
+            constants.MODULE_CLASS: [],
+            constants.MODULE_COMPATIBILITY_SUITES: []}
+        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, t_infos[0])
+
+    @mock.patch.object(module_finder.ModuleFinder, '_get_build_targets',
+                       return_value=uc.MODULE_BUILD_TARGETS)
+    @mock.patch.object(module_finder.ModuleFinder, '_is_vts_module',
+                       return_value=False)
+    @mock.patch.object(test_finder_utils, 'find_parent_module_dir',
+                       return_value=os.path.relpath(uc.TEST_DATA_DIR, uc.ROOT))
+    #pylint: disable=unused-argument
+    def test_find_test_by_path_part_2(self, _find_parent, _is_vts, _get_build):
+        """Test find_test_by_path for directories."""
+        self.mod_finder.module_info.is_auto_gen_test_config.return_value = False
+        self.mod_finder.module_info.is_robolectric_test.return_value = False
+        self.mod_finder.module_info.has_test_config.return_value = True
+        # Dir with java files in it, should run as package
+        class_dir = os.path.join(uc.TEST_DATA_DIR, 'path_testing')
+        self.mod_finder.module_info.get_module_names.return_value = [uc.MODULE_NAME]
+        self.mod_finder.module_info.get_module_info.return_value = {
+            constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+            constants.MODULE_NAME: uc.MODULE_NAME,
+            constants.MODULE_CLASS: [],
+            constants.MODULE_COMPATIBILITY_SUITES: []}
+        t_infos = self.mod_finder.find_test_by_path(class_dir)
+        unittest_utils.assert_equal_testinfos(
+            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,
+            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]
+        self.mod_finder.module_info.get_module_info.return_value = {
+            constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+            constants.MODULE_NAME: uc.CC_MODULE_NAME,
+            constants.MODULE_CLASS: [],
+            constants.MODULE_COMPATIBILITY_SUITES: []}
+        t_infos = self.mod_finder.find_test_by_path(class_dir)
+        unittest_utils.assert_equal_testinfos(
+            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')
+    @mock.patch('subprocess.check_output', return_value=uc.CC_FIND_ONE)
+    @mock.patch('os.path.isfile', side_effect=unittest_utils.isfile_side_effect)
+    @mock.patch('os.path.isdir', return_value=True)
+    #pylint: disable=unused-argument
+    def test_find_test_by_cc_class_name(self, _isdir, _isfile,
+                                        mock_checkoutput, mock_build,
+                                        _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
+        self.mod_finder.module_info.is_robolectric_test.return_value = False
+        self.mod_finder.module_info.has_test_config.return_value = True
+        self.mod_finder.module_info.get_module_names.return_value = [uc.CC_MODULE_NAME]
+        self.mod_finder.module_info.get_module_info.return_value = {
+            constants.MODULE_INSTALLED: DEFAULT_INSTALL_PATH,
+            constants.MODULE_NAME: uc.CC_MODULE_NAME,
+            constants.MODULE_CLASS: [],
+            constants.MODULE_COMPATIBILITY_SUITES: []}
+        t_infos = self.mod_finder.find_test_by_cc_class_name(uc.CC_CLASS_NAME)
+        unittest_utils.assert_equal_testinfos(
+            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,
+            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, 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, 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, t_infos[0],
+            CC_CLASS_INFO_MODULE_2)
+
+    def test_get_testable_modules_with_ld(self):
+        """Test get_testable_modules_with_ld"""
+        self.mod_finder.module_info.get_testable_modules.return_value = [
+            uc.MODULE_NAME, uc.MODULE2_NAME]
+        # Without a misfit constraint
+        ld1 = self.mod_finder.get_testable_modules_with_ld(uc.TYPO_MODULE_NAME)
+        self.assertEqual([[16, uc.MODULE2_NAME], [1, uc.MODULE_NAME]], ld1)
+        # With a misfit constraint
+        ld2 = self.mod_finder.get_testable_modules_with_ld(uc.TYPO_MODULE_NAME, 2)
+        self.assertEqual([[1, uc.MODULE_NAME]], ld2)
+
+    def test_get_fuzzy_searching_modules(self):
+        """Test get_fuzzy_searching_modules"""
+        self.mod_finder.module_info.get_testable_modules.return_value = [
+            uc.MODULE_NAME, uc.MODULE2_NAME]
+        result = self.mod_finder.get_fuzzy_searching_results(uc.TYPO_MODULE_NAME)
+        self.assertEqual(uc.MODULE_NAME, result[0])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_finders/suite_plan_finder.py b/atest/test_finders/suite_plan_finder.py
new file mode 100644
index 0000000..a33da2d
--- /dev/null
+++ b/atest/test_finders/suite_plan_finder.py
@@ -0,0 +1,158 @@
+# Copyright 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.
+
+"""
+Suite Plan Finder class.
+"""
+
+import logging
+import os
+import re
+
+# pylint: disable=import-error
+import constants
+from test_finders import test_finder_base
+from test_finders import test_finder_utils
+from test_finders import test_info
+from test_runners import suite_plan_test_runner
+
+_SUITE_PLAN_NAME_RE = re.compile(r'^.*\/(?P<suite>.*)-tradefed\/res\/config\/'
+                                 r'(?P<suite_plan_name>.*).xml$')
+
+
+class SuitePlanFinder(test_finder_base.TestFinderBase):
+    """Suite Plan Finder class."""
+    NAME = 'SUITE_PLAN'
+    _SUITE_PLAN_TEST_RUNNER = suite_plan_test_runner.SuitePlanTestRunner.NAME
+
+    def __init__(self, module_info=None):
+        super(SuitePlanFinder, self).__init__()
+        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
+        self.mod_info = module_info
+        self.suite_plan_dirs = self._get_suite_plan_dirs()
+
+    def _get_mod_paths(self, module_name):
+        """Return the paths of the given module name."""
+        if self.mod_info:
+            return self.mod_info.get_paths(module_name)
+        return []
+
+    def _get_suite_plan_dirs(self):
+        """Get suite plan dirs from MODULE_INFO based on targets.
+
+        Strategy:
+            Search module-info.json using SUITE_PLANS to get all the suite
+            plan dirs.
+
+        Returns:
+            A tuple of lists of strings of suite plan dir rel to repo root.
+            None if the path can not be found in module-info.json.
+        """
+        return [d for x in constants.SUITE_PLANS for d in
+                self._get_mod_paths(x+'-tradefed') if d is not None]
+
+    def _get_test_info_from_path(self, path, suite_name=None):
+        """Get the test info from the result of using regular expression
+        matching with the give path.
+
+        Args:
+            path: A string of the test's absolute or relative path.
+            suite_name: A string of the suite name.
+
+        Returns:
+            A populated TestInfo namedtuple if regular expression
+            matches, else None.
+        """
+        # Don't use names that simply match the path,
+        # must be the actual name used by *TS to run the test.
+        match = _SUITE_PLAN_NAME_RE.match(path)
+        if not match:
+            logging.error('Suite plan test outside config dir: %s', path)
+            return None
+        suite = match.group('suite')
+        suite_plan_name = match.group('suite_plan_name')
+        if suite_name:
+            if suite_plan_name != suite_name:
+                logging.warn('Input (%s) not valid suite plan name, '
+                             'did you mean: %s?', suite_name, suite_plan_name)
+                return None
+        return test_info.TestInfo(
+            test_name=suite_plan_name,
+            test_runner=self._SUITE_PLAN_TEST_RUNNER,
+            build_targets=set([suite]),
+            suite=suite)
+
+    def find_test_by_suite_path(self, suite_path):
+        """Find the first test info matching the given path.
+
+        Strategy:
+            If suite_path is to file --> Return TestInfo if the file
+            exists in the suite plan dirs, else return None.
+            If suite_path is to dir --> Return None
+
+        Args:
+            suite_path: A string of the path to the test's file or dir.
+
+        Returns:
+            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.
+        if not path.endswith('.xml'):
+            return None
+        path = os.path.realpath(path)
+        suite_plan_dir = test_finder_utils.get_int_dir_from_path(
+            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 None
+
+    def find_test_by_suite_name(self, suite_name):
+        """Find the test for the given suite name.
+
+        Strategy:
+            If suite_name is cts --> Return TestInfo to indicate suite runner
+            to make cts and run test using cts-tradefed.
+            If suite_name is cts-common --> Return TestInfo to indicate suite
+            runner to make cts and run test using cts-tradefed if file exists
+            in the suite plan dirs, else return None.
+
+        Args:
+            suite_name: A string of suite name.
+
+        Returns:
+            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:
+            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))
+        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
new file mode 100755
index 0000000..0fed2d2
--- /dev/null
+++ b/atest/test_finders/suite_plan_finder_unittest.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+"""Unittests for suite_plan_finder."""
+
+import os
+import unittest
+import mock
+
+# pylint: disable=import-error
+import unittest_constants as uc
+import unittest_utils
+from test_finders import test_finder_utils
+from test_finders import test_info
+from test_finders import suite_plan_finder
+from test_runners import suite_plan_test_runner
+
+
+# pylint: disable=protected-access
+class SuitePlanFinderUnittests(unittest.TestCase):
+    """Unit tests for suite_plan_finder.py"""
+
+    def setUp(self):
+        """Set up stuff for testing."""
+        self.suite_plan_finder = suite_plan_finder.SuitePlanFinder()
+        self.suite_plan_finder.suite_plan_dirs = [os.path.join(uc.ROOT, uc.CTS_INT_DIR)]
+        self.suite_plan_finder.root_dir = uc.ROOT
+
+    def test_get_test_info_from_path(self):
+        """Test _get_test_info_from_path.
+        Strategy:
+            If suite_path is to cts file -->
+                test_info: test_name=cts,
+                           test_runner=TestSuiteTestRunner,
+                           build_target=set(['cts']
+                           suite='cts')
+            If suite_path is to cts-common file -->
+                test_info: test_name=cts-common,
+                           test_runner=TestSuiteTestRunner,
+                           build_target=set(['cts']
+                           suite='cts')
+            If suite_path is to common file --> test_info: None
+            If suite_path is to non-existing file --> test_info: None
+        """
+        suite_plan = 'cts'
+        path = os.path.join(uc.ROOT, uc.CTS_INT_DIR, suite_plan+'.xml')
+        want_info = test_info.TestInfo(test_name=suite_plan,
+                                       test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
+                                       build_targets={suite_plan},
+                                       suite=suite_plan)
+        unittest_utils.assert_equal_testinfos(
+            self, want_info, self.suite_plan_finder._get_test_info_from_path(path))
+
+        suite_plan = 'cts-common'
+        path = os.path.join(uc.ROOT, uc.CTS_INT_DIR, suite_plan+'.xml')
+        want_info = test_info.TestInfo(test_name=suite_plan,
+                                       test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
+                                       build_targets={'cts'},
+                                       suite='cts')
+        unittest_utils.assert_equal_testinfos(
+            self, want_info, self.suite_plan_finder._get_test_info_from_path(path))
+
+        suite_plan = 'common'
+        path = os.path.join(uc.ROOT, uc.CTS_INT_DIR, 'cts-common.xml')
+        want_info = None
+        unittest_utils.assert_equal_testinfos(
+            self, want_info, self.suite_plan_finder._get_test_info_from_path(path, suite_plan))
+
+        path = os.path.join(uc.ROOT, 'cts-common.xml')
+        want_info = None
+        unittest_utils.assert_equal_testinfos(
+            self, want_info, self.suite_plan_finder._get_test_info_from_path(path))
+
+    @mock.patch.object(test_finder_utils, 'search_integration_dirs')
+    def test_find_test_by_suite_name(self, _search):
+        """Test find_test_by_suite_name.
+        Strategy:
+            suite_name: cts --> test_info: test_name=cts,
+                                           test_runner=TestSuiteTestRunner,
+                                           build_target=set(['cts']
+                                           suite='cts')
+            suite_name: CTS --> test_info: None
+            suite_name: cts-common --> test_info: test_name=cts-common,
+                                                  test_runner=TestSuiteTestRunner,
+                                                  build_target=set(['cts'],
+                                                  suite='cts')
+        """
+        suite_name = 'cts'
+        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={suite_name},
+                                       suite=suite_name)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
+
+        suite_name = 'CTS'
+        _search.return_value = None
+        t_info = self.suite_plan_finder.find_test_by_suite_name(suite_name)
+        want_info = None
+        unittest_utils.assert_equal_testinfos(self, t_info, want_info)
+
+        suite_name = 'cts-common'
+        suite = 'cts'
+        _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[0], want_info)
+
+    @mock.patch('os.path.realpath',
+                side_effect=unittest_utils.realpath_side_effect)
+    @mock.patch('os.path.isdir', return_value=True)
+    @mock.patch('os.path.isfile', return_value=True)
+    @mock.patch.object(test_finder_utils, 'get_int_dir_from_path')
+    @mock.patch('os.path.exists', return_value=True)
+    def test_find_suite_plan_test_by_suite_path(self, _exists, _find, _isfile, _isdir, _real):
+        """Test find_test_by_suite_name.
+        Strategy:
+            suite_name: cts.xml --> test_info:
+                                        test_name=cts,
+                                        test_runner=TestSuiteTestRunner,
+                                        build_target=set(['cts']
+                                        suite='cts')
+            suite_name: cts-common.xml --> test_info:
+                                               test_name=cts-common,
+                                               test_runner=TestSuiteTestRunner,
+                                               build_target=set(['cts'],
+                                               suite='cts')
+            suite_name: cts-camera.xml --> test_info:
+                                               test_name=cts-camera,
+                                               test_runner=TestSuiteTestRunner,
+                                               build_target=set(['cts'],
+                                               suite='cts')
+        """
+        suite_int_name = 'cts'
+        suite = 'cts'
+        path = os.path.join(uc.CTS_INT_DIR, suite_int_name + '.xml')
+        _find.return_value = uc.CTS_INT_DIR
+        t_info = self.suite_plan_finder.find_test_by_suite_path(path)
+        want_info = test_info.TestInfo(test_name=suite_int_name,
+                                       test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
+                                       build_targets=set([suite]),
+                                       suite=suite)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
+
+        suite_int_name = 'cts-common'
+        suite = 'cts'
+        path = os.path.join(uc.CTS_INT_DIR, suite_int_name + '.xml')
+        _find.return_value = uc.CTS_INT_DIR
+        t_info = self.suite_plan_finder.find_test_by_suite_path(path)
+        want_info = test_info.TestInfo(test_name=suite_int_name,
+                                       test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
+                                       build_targets=set([suite]),
+                                       suite=suite)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
+
+        suite_int_name = 'cts-camera'
+        suite = 'cts'
+        path = os.path.join(uc.CTS_INT_DIR, suite_int_name + '.xml')
+        _find.return_value = uc.CTS_INT_DIR
+        t_info = self.suite_plan_finder.find_test_by_suite_path(path)
+        want_info = test_info.TestInfo(test_name=suite_int_name,
+                                       test_runner=suite_plan_test_runner.SuitePlanTestRunner.NAME,
+                                       build_targets=set([suite]),
+                                       suite=suite)
+        unittest_utils.assert_equal_testinfos(self, t_info[0], want_info)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_finders/test_finder_base.py b/atest/test_finders/test_finder_base.py
new file mode 100644
index 0000000..14fc079
--- /dev/null
+++ b/atest/test_finders/test_finder_base.py
@@ -0,0 +1,54 @@
+# Copyright 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.
+
+"""
+Test finder base class.
+"""
+from collections import namedtuple
+
+
+Finder = namedtuple('Finder', ['test_finder_instance', 'find_method',
+                               'finder_info'])
+
+
+def find_method_register(cls):
+    """Class decorater to find all registered find methods."""
+    cls.find_methods = []
+    cls.get_all_find_methods = lambda x: x.find_methods
+    for methodname in dir(cls):
+        method = getattr(cls, methodname)
+        if hasattr(method, '_registered'):
+            cls.find_methods.append(Finder(None, method, None))
+    return cls
+
+
+def register():
+    """Decorator to register find methods."""
+
+    def wrapper(func):
+        """Wrapper for the register decorator."""
+        #pylint: disable=protected-access
+        func._registered = True
+        return func
+    return wrapper
+
+
+# This doesn't really do anything since there are no find methods defined but
+# it's here anyways as an example for other test type classes.
+@find_method_register
+class TestFinderBase(object):
+    """Base class for test finder class."""
+
+    def __init__(self, *args, **kwargs):
+        pass
diff --git a/atest/test_finders/test_finder_utils.py b/atest/test_finders/test_finder_utils.py
new file mode 100644
index 0000000..2e8ec64
--- /dev/null
+++ b/atest/test_finders/test_finder_utils.py
@@ -0,0 +1,957 @@
+# Copyright 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.
+
+"""
+Utils for finder classes.
+"""
+
+from __future__ import print_function
+import logging
+import multiprocessing
+import os
+import pickle
+import re
+import subprocess
+import time
+import xml.etree.ElementTree as ET
+
+# pylint: disable=import-error
+import atest_decorator
+import atest_error
+import atest_enum
+import constants
+
+from metrics import metrics_utils
+
+# Helps find apk files listed in a test config (AndroidTest.xml) file.
+# Matches "filename.apk" in <option name="foo", value="filename.apk" />
+# 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 checking if TEST or TEST_F is in a cc file or not.
+_CC_CLASS_RE = re.compile(r'^[ ]*TEST(_F|_P)?[ ]*\(', 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})\).*'
+# 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)
+# Matches install paths in module_info to install location(host or device).
+_HOST_PATH_RE = re.compile(r'.*\/host\/.*', re.I)
+_DEVICE_PATH_RE = re.compile(r'.*\/target\/.*', re.I)
+
+# 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)
+# 1. QUALIFIED_CLASS: Like CLASS but also contains the package in front like
+#                     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.
+
+FIND_REFERENCE_TYPE = atest_enum.AtestEnum(['CLASS', 'QUALIFIED_CLASS',
+                                            'PACKAGE', 'INTEGRATION', 'CC_CLASS'])
+# Get cpu count.
+_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} {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} {1} -wholename "
+                                     r"'*{2}.xml' -print",
+    # 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"
+}
+
+# Map ref_type with its index file.
+FIND_INDEXES = {
+    FIND_REFERENCE_TYPE.CLASS: constants.CLASS_INDEX,
+    FIND_REFERENCE_TYPE.QUALIFIED_CLASS: constants.QCLASS_INDEX,
+    FIND_REFERENCE_TYPE.PACKAGE: constants.PACKAGE_INDEX,
+    FIND_REFERENCE_TYPE.INTEGRATION: constants.INT_INDEX,
+    FIND_REFERENCE_TYPE.CC_CLASS: constants.CC_CLASS_INDEX
+}
+
+# XML parsing related constants.
+_COMPATIBILITY_PACKAGE_PREFIX = "com.android.compatibility"
+_CTS_JAR = "cts-tradefed"
+_XML_PUSH_DELIM = '->'
+_APK_SUFFIX = '.apk'
+# Setup script for device perf tests.
+_PERF_SETUP_LABEL = 'perf-setup.sh'
+
+# XML tags.
+_XML_NAME = 'name'
+_XML_VALUE = 'value'
+
+# VTS xml parsing constants.
+_VTS_TEST_MODULE = 'test-module-name'
+_VTS_MODULE = 'module-name'
+_VTS_BINARY_SRC = 'binary-test-source'
+_VTS_PUSH_GROUP = 'push-group'
+_VTS_PUSH = 'push'
+_VTS_BINARY_SRC_DELIM = '::'
+_VTS_PUSH_DIR = os.path.join(os.environ.get(constants.ANDROID_BUILD_TOP, ''),
+                             'test', 'vts', 'tools', 'vts-tradefed', 'res',
+                             'push_groups')
+_VTS_PUSH_SUFFIX = '.push'
+_VTS_BITNESS = 'append-bitness'
+_VTS_BITNESS_TRUE = 'true'
+_VTS_BITNESS_32 = '32'
+_VTS_BITNESS_64 = '64'
+_VTS_TEST_FILE = 'test-file-name'
+_VTS_APK = 'apk'
+# Matches 'DATA/target' in '_32bit::DATA/target'
+_VTS_BINARY_SRC_DELIM_RE = re.compile(r'.*::(?P<target>.*)$')
+_VTS_OUT_DATA_APP_PATH = 'DATA/app'
+
+# pylint: disable=inconsistent-return-statements
+def split_methods(user_input):
+    """Split user input string into test reference and list of methods.
+
+    Args:
+        user_input: A string of the user's input.
+                    Examples:
+                        class_name
+                        class_name#method1,method2
+                        path
+                        path#method1,method2
+    Returns:
+        A tuple. First element is String of test ref and second element is
+        a set of method name strings or empty list if no methods included.
+    Exception:
+        atest_error.TooManyMethodsError raised when input string is trying to
+        specify too many methods in a single positional argument.
+
+        Examples of unsupported input strings:
+            module:class#method,class#method
+            class1#method,class2#method
+            path1#method,path2#method
+    """
+    parts = user_input.split('#')
+    if len(parts) == 1:
+        return parts[0], frozenset()
+    elif len(parts) == 2:
+        return parts[0], frozenset(parts[1].split(','))
+    raise atest_error.TooManyMethodsError(
+        'Too many methods specified with # character in user input: %s.'
+        '\n\nOnly one class#method combination supported per positional'
+        ' argument. Multiple classes should be separated by spaces: '
+        'class#method class#method')
+
+
+# pylint: disable=inconsistent-return-statements
+def get_fully_qualified_class_name(test_path):
+    """Parse the fully qualified name from the class java file.
+
+    Args:
+        test_path: A string of absolute path to the java class file.
+
+    Returns:
+        A string of the fully qualified class name.
+
+    Raises:
+        atest_error.MissingPackageName if no class name can be found.
+    """
+    with open(test_path) as class_file:
+        for line in class_file:
+            match = _PACKAGE_RE.match(line)
+            if match:
+                package = match.group('package')
+                cls = os.path.splitext(os.path.split(test_path)[1])[0]
+                return '%s.%s' % (package, cls)
+    raise atest_error.MissingPackageNameError('%s: Test class java file'
+                                              'does not contain a package'
+                                              'name.'% test_path)
+
+
+def has_cc_class(test_path):
+    """Find out if there is any test case in the cc file.
+
+    Args:
+        test_path: A string of absolute path to the cc file.
+
+    Returns:
+        Boolean: has cc class in test_path or not.
+    """
+    with open(test_path) as class_file:
+        for line in class_file:
+            match = _CC_CLASS_RE.match(line)
+            if match:
+                return True
+    return False
+
+
+def get_package_name(file_name):
+    """Parse the package name from a java file.
+
+    Args:
+        file_name: A string of the absolute path to the java file.
+
+    Returns:
+        A string of the package name or None
+      """
+    with open(file_name) as data:
+        for line in data:
+            match = _PACKAGE_RE.match(line)
+            if match:
+                return match.group('package')
+
+
+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:
+    /<some_root>/cts/tests/jank/src/android/jank/cts/ui/CtsDeviceJankUi.java
+
+    Args:
+        output: A string or list output of a unix 'find' command.
+        methods: A set of method names.
+
+    Returns:
+        A list of the test paths or None if output is '' or None.
+    """
+    if not output:
+        return None
+    verified_tests = set()
+    if isinstance(output, str):
+        output = output.splitlines()
+    for test in output:
+        # compare CC_OUTPUT_RE with output
+        match_obj = constants.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:
+            # TODO (b/138997521) - Atest checks has_method_in_file of a class
+            #  without traversing its parent classes. A workaround for this is
+            #  do not check has_method_in_file. Uncomment below when a solution
+            #  to it is applied.
+            # 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 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 list of paths.
+    """
+    count = len(tests)
+    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)))
+        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)
+
+
+@atest_decorator.static_var("cached_ignore_dirs", [])
+def _get_ignored_dirs():
+    """Get ignore dirs in find command.
+
+    Since we can't construct a single find cmd to find the target and
+    filter-out the dir with .out-dir, .find-ignore and $OUT-DIR. We have
+    to run the 1st find cmd to find these dirs. Then, we can use these
+    results to generate the real find cmd.
+
+    Return:
+        A list of the ignore dirs.
+    """
+    out_dirs = _get_ignored_dirs.cached_ignore_dirs
+    if not out_dirs:
+        build_top = os.environ.get(constants.ANDROID_BUILD_TOP)
+        find_out_dir_cmd = (r'find %s -maxdepth 2 '
+                            r'-type f \( -name ".out-dir" -o -name '
+                            r'".find-ignore" \)') % build_top
+        out_files = subprocess.check_output(find_out_dir_cmd, shell=True)
+        # Get all dirs with .out-dir or .find-ignore
+        if out_files:
+            out_files = out_files.splitlines()
+            for out_file in out_files:
+                if out_file:
+                    out_dirs.append(os.path.dirname(out_file.strip()))
+        # Get the out folder if user specified $OUT_DIR
+        custom_out_dir = os.environ.get(constants.ANDROID_OUT_DIR)
+        if custom_out_dir:
+            user_out_dir = None
+            if os.path.isabs(custom_out_dir):
+                user_out_dir = custom_out_dir
+            else:
+                user_out_dir = os.path.join(build_top, custom_out_dir)
+            # only ignore the out_dir when it under $ANDROID_BUILD_TOP
+            if build_top in user_out_dir:
+                if user_out_dir not in out_dirs:
+                    out_dirs.append(user_out_dir)
+        _get_ignored_dirs.cached_ignore_dirs = out_dirs
+    return out_dirs
+
+
+def _get_prune_cond_of_ignored_dirs():
+    """Get the prune condition of ignore dirs.
+
+    Generation a string of the prune condition in the find command.
+    It will filter-out the dir with .out-dir, .find-ignore and $OUT-DIR.
+    Because they are the out dirs, we don't have to find them.
+
+    Return:
+        A string of the prune condition of the ignore dirs.
+    """
+    out_dirs = _get_ignored_dirs()
+    prune_cond = r'-type d \( -name ".*"'
+    for out_dir in out_dirs:
+        prune_cond += r' -o -path %s' % out_dir
+    prune_cond += r' \) -prune -o'
+    return prune_cond
+
+
+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 list of the path to the target.
+        If the search_dir is inexistent, None will be returned.
+    """
+    # If module_info.json is outdated, finding in the search_dir can result in
+    # raising exception. Return null immediately can guild users to run
+    # --rebuild-module-info to resolve the problem.
+    if not os.path.isdir(search_dir):
+        logging.debug('\'%s\' does not exist!', search_dir)
+        return None
+    ref_name = FIND_REFERENCE_TYPE[ref_type]
+    start = time.time()
+    if os.path.isfile(FIND_INDEXES[ref_type]):
+        _dict, out = {}, None
+        with open(FIND_INDEXES[ref_type], 'rb') as index:
+            try:
+                _dict = pickle.load(index)
+            except (IOError, EOFError, pickle.UnpicklingError) as err:
+                logging.debug('Exception raised: %s', err)
+                metrics_utils.handle_exc_and_send_exit_event(
+                    constants.ACCESS_CACHE_FAILURE)
+                os.remove(FIND_INDEXES[ref_type])
+        if _dict.get(target):
+            logging.debug('Found %s in %s', target, FIND_INDEXES[ref_type])
+            out = [path for path in _dict.get(target) if search_dir in path]
+    else:
+        prune_cond = _get_prune_cond_of_ignored_dirs()
+        if '.' in target:
+            target = target.replace('.', '/')
+        find_cmd = FIND_CMDS[ref_type].format(search_dir, prune_cond, target)
+        logging.debug('Executing %s find cmd: %s', ref_name, find_cmd)
+        out = subprocess.check_output(find_cmd, shell=True)
+        logging.debug('%s find cmd out: %s', ref_name, out)
+    logging.debug('%s find completed in %ss', ref_name, time.time() - start)
+    return extract_test_path(out, methods)
+
+
+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:
+        search_dir: A string of the dirpath to search in.
+        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 list of the path to the java/cc file.
+    """
+    if is_native_test:
+        ref_type = FIND_REFERENCE_TYPE.CC_CLASS
+    elif '.' in class_name:
+        ref_type = FIND_REFERENCE_TYPE.QUALIFIED_CLASS
+    else:
+        ref_type = FIND_REFERENCE_TYPE.CLASS
+    return run_find_cmd(ref_type, search_dir, class_name, methods)
+
+
+def is_equal_or_sub_dir(sub_dir, parent_dir):
+    """Return True sub_dir is sub dir or equal to parent_dir.
+
+    Args:
+      sub_dir: A string of the sub directory path.
+      parent_dir: A string of the parent directory path.
+
+    Returns:
+        A boolean of whether both are dirs and sub_dir is sub of parent_dir
+        or is equal to parent_dir.
+    """
+    # avoid symlink issues with real path
+    parent_dir = os.path.realpath(parent_dir)
+    sub_dir = os.path.realpath(sub_dir)
+    if not os.path.isdir(sub_dir) or not os.path.isdir(parent_dir):
+        return False
+    return os.path.commonprefix([sub_dir, parent_dir]) == parent_dir
+
+
+def find_parent_module_dir(root_dir, start_dir, module_info):
+    """From current dir search up file tree until root dir for module dir.
+
+    Args:
+        root_dir: A string  of the dir that is the parent of the start dir.
+        start_dir: A string of the dir to start searching up from.
+        module_info: ModuleInfo object containing module information from the
+                     build system.
+
+    Returns:
+        A string of the module dir relative to root, None if no Module Dir
+        found. There may be multiple testable modules at this level.
+
+    Exceptions:
+        ValueError: Raised if cur_dir not dir or not subdir of root dir.
+    """
+    if not is_equal_or_sub_dir(start_dir, root_dir):
+        raise ValueError('%s not in repo %s' % (start_dir, root_dir))
+    auto_gen_dir = None
+    current_dir = start_dir
+    while current_dir != root_dir:
+        # TODO (b/112904944) - migrate module_finder functions to here and
+        # reuse them.
+        rel_dir = os.path.relpath(current_dir, root_dir)
+        # Check if actual config file here
+        if os.path.isfile(os.path.join(current_dir, constants.MODULE_CONFIG)):
+            return rel_dir
+        # Check module_info if auto_gen config or robo (non-config) here
+        for mod in module_info.path_to_module_info.get(rel_dir, []):
+            if module_info.is_robolectric_module(mod):
+                return rel_dir
+            for test_config in mod.get(constants.MODULE_TEST_CONFIG, []):
+                if os.path.isfile(os.path.join(root_dir, test_config)):
+                    return rel_dir
+            if mod.get('auto_test_config'):
+                auto_gen_dir = rel_dir
+                # Don't return for auto_gen, keep checking for real config, because
+                # common in cts for class in apk that's in hostside test setup.
+        current_dir = os.path.dirname(current_dir)
+    return auto_gen_dir
+
+
+def get_targets_from_xml(xml_file, module_info):
+    """Retrieve build targets from the given xml.
+
+    Just a helper func on top of get_targets_from_xml_root.
+
+    Args:
+        xml_file: abs path to xml file.
+        module_info: ModuleInfo class used to verify targets are valid modules.
+
+    Returns:
+        A set of build targets based on the signals found in the xml file.
+    """
+    xml_root = ET.parse(xml_file).getroot()
+    return get_targets_from_xml_root(xml_root, module_info)
+
+
+def _get_apk_target(apk_target):
+    """Return the sanitized apk_target string from the xml.
+
+    The apk_target string can be of 2 forms:
+      - apk_target.apk
+      - apk_target.apk->/path/to/install/apk_target.apk
+
+    We want to return apk_target in both cases.
+
+    Args:
+        apk_target: String of target name to clean.
+
+    Returns:
+        String of apk_target to build.
+    """
+    apk = apk_target.split(_XML_PUSH_DELIM, 1)[0].strip()
+    return apk[:-len(_APK_SUFFIX)]
+
+
+def _is_apk_target(name, value):
+    """Return True if XML option is an apk target.
+
+    We have some scenarios where an XML option can be an apk target:
+      - value is an apk file.
+      - name is a 'push' option where value holds the apk_file + other stuff.
+
+    Args:
+        name: String name of XML option.
+        value: String value of the XML option.
+
+    Returns:
+        True if it's an apk target we should build, False otherwise.
+    """
+    if _APK_RE.match(value):
+        return True
+    if name == 'push' and value.endswith(_APK_SUFFIX):
+        return True
+    return False
+
+
+def get_targets_from_xml_root(xml_root, module_info):
+    """Retrieve build targets from the given xml root.
+
+    We're going to pull the following bits of info:
+      - Parse any .apk files listed in the config file.
+      - Parse option value for "test-module-name" (for vts tests).
+      - Look for the perf script.
+
+    Args:
+        module_info: ModuleInfo class used to verify targets are valid modules.
+        xml_root: ElementTree xml_root for us to look through.
+
+    Returns:
+        A set of build targets based on the signals found in the xml file.
+    """
+    targets = set()
+    option_tags = xml_root.findall('.//option')
+    for tag in option_tags:
+        target_to_add = None
+        name = tag.attrib[_XML_NAME].strip()
+        value = tag.attrib[_XML_VALUE].strip()
+        if _is_apk_target(name, value):
+            target_to_add = _get_apk_target(value)
+        elif _PERF_SETUP_LABEL in value:
+            targets.add(_PERF_SETUP_LABEL)
+            continue
+
+        # Let's make sure we can actually build the target.
+        if target_to_add and module_info.is_module(target_to_add):
+            targets.add(target_to_add)
+        elif target_to_add:
+            logging.warning('Build target (%s) not present in module info, '
+                            'skipping build', target_to_add)
+
+    # TODO (b/70813166): Remove this lookup once all runtime dependencies
+    # can be listed as a build dependencies or are in the base test harness.
+    nodes_with_class = xml_root.findall(".//*[@class]")
+    for class_attr in nodes_with_class:
+        fqcn = class_attr.attrib['class'].strip()
+        if fqcn.startswith(_COMPATIBILITY_PACKAGE_PREFIX):
+            targets.add(_CTS_JAR)
+    logging.debug('Targets found in config file: %s', targets)
+    return targets
+
+
+def _get_vts_push_group_targets(push_file, rel_out_dir):
+    """Retrieve vts push group build targets.
+
+    A push group file is a file that list out test dependencies and other push
+    group files. Go through the push file and gather all the test deps we need.
+
+    Args:
+        push_file: Name of the push file in the VTS
+        rel_out_dir: Abs path to the out dir to help create vts build targets.
+
+    Returns:
+        Set of string which represent build targets.
+    """
+    targets = set()
+    full_push_file_path = os.path.join(_VTS_PUSH_DIR, push_file)
+    # pylint: disable=invalid-name
+    with open(full_push_file_path) as f:
+        for line in f:
+            target = line.strip()
+            # Skip empty lines.
+            if not target:
+                continue
+
+            # This is a push file, get the targets from it.
+            if target.endswith(_VTS_PUSH_SUFFIX):
+                targets |= _get_vts_push_group_targets(line.strip(),
+                                                       rel_out_dir)
+                continue
+            sanitized_target = target.split(_XML_PUSH_DELIM, 1)[0].strip()
+            targets.add(os.path.join(rel_out_dir, sanitized_target))
+    return targets
+
+
+def _specified_bitness(xml_root):
+    """Check if the xml file contains the option append-bitness.
+
+    Args:
+        xml_root: abs path to xml file.
+
+    Returns:
+        True if xml specifies to append-bitness, False otherwise.
+    """
+    option_tags = xml_root.findall('.//option')
+    for tag in option_tags:
+        value = tag.attrib[_XML_VALUE].strip()
+        name = tag.attrib[_XML_NAME].strip()
+        if name == _VTS_BITNESS and value == _VTS_BITNESS_TRUE:
+            return True
+    return False
+
+
+def _get_vts_binary_src_target(value, rel_out_dir):
+    """Parse out the vts binary src target.
+
+    The value can be in the following pattern:
+      - {_32bit,_64bit,_IPC32_32bit}::DATA/target (DATA/target)
+      - DATA/target->/data/target (DATA/target)
+      - out/host/linx-x86/bin/VtsSecuritySelinuxPolicyHostTest (the string as
+        is)
+
+    Args:
+        value: String of the XML option value to parse.
+        rel_out_dir: String path of out dir to prepend to target when required.
+
+    Returns:
+        String of the target to build.
+    """
+    # We'll assume right off the bat we can use the value as is and modify it if
+    # necessary, e.g. out/host/linux-x86/bin...
+    target = value
+    # _32bit::DATA/target
+    match = _VTS_BINARY_SRC_DELIM_RE.match(value)
+    if match:
+        target = os.path.join(rel_out_dir, match.group('target'))
+    # DATA/target->/data/target
+    elif _XML_PUSH_DELIM in value:
+        target = value.split(_XML_PUSH_DELIM, 1)[0].strip()
+        target = os.path.join(rel_out_dir, target)
+    return target
+
+
+def get_plans_from_vts_xml(xml_file):
+    """Get configs which are included by xml_file.
+
+    We're looking for option(include) to get all dependency plan configs.
+
+    Args:
+        xml_file: Absolute path to xml file.
+
+    Returns:
+        A set of plan config paths which are depended by xml_file.
+    """
+    if not os.path.exists(xml_file):
+        raise atest_error.XmlNotExistError('%s: The xml file does'
+                                           'not exist' % xml_file)
+    plans = set()
+    xml_root = ET.parse(xml_file).getroot()
+    plans.add(xml_file)
+    option_tags = xml_root.findall('.//include')
+    if not option_tags:
+        return plans
+    # Currently, all vts xmls live in the same dir :
+    # https://android.googlesource.com/platform/test/vts/+/master/tools/vts-tradefed/res/config/
+    # If the vts plans start using folders to organize the plans, the logic here
+    # should be changed.
+    xml_dir = os.path.dirname(xml_file)
+    for tag in option_tags:
+        name = tag.attrib[_XML_NAME].strip()
+        plans |= get_plans_from_vts_xml(os.path.join(xml_dir, name + ".xml"))
+    return plans
+
+
+def get_targets_from_vts_xml(xml_file, rel_out_dir, module_info):
+    """Parse a vts xml for test dependencies we need to build.
+
+    We have a separate vts parsing function because we make a big assumption
+    on the targets (the way they're formatted and what they represent) and we
+    also create these build targets in a very special manner as well.
+    The 6 options we're looking for are:
+      - binary-test-source
+      - push-group
+      - push
+      - test-module-name
+      - test-file-name
+      - apk
+
+    Args:
+        module_info: ModuleInfo class used to verify targets are valid modules.
+        rel_out_dir: Abs path to the out dir to help create vts build targets.
+        xml_file: abs path to xml file.
+
+    Returns:
+        A set of build targets based on the signals found in the xml file.
+    """
+    xml_root = ET.parse(xml_file).getroot()
+    targets = set()
+    option_tags = xml_root.findall('.//option')
+    for tag in option_tags:
+        value = tag.attrib[_XML_VALUE].strip()
+        name = tag.attrib[_XML_NAME].strip()
+        if name in [_VTS_TEST_MODULE, _VTS_MODULE]:
+            if module_info.is_module(value):
+                targets.add(value)
+            else:
+                logging.warning('vts test module (%s) not present in module '
+                                'info, skipping build', value)
+        elif name == _VTS_BINARY_SRC:
+            targets.add(_get_vts_binary_src_target(value, rel_out_dir))
+        elif name == _VTS_PUSH_GROUP:
+            # Look up the push file and parse out build artifacts (as well as
+            # other push group files to parse).
+            targets |= _get_vts_push_group_targets(value, rel_out_dir)
+        elif name == _VTS_PUSH:
+            # Parse out the build artifact directly.
+            push_target = value.split(_XML_PUSH_DELIM, 1)[0].strip()
+            # If the config specified append-bitness, append the bits suffixes
+            # to the target.
+            if _specified_bitness(xml_root):
+                targets.add(os.path.join(rel_out_dir, push_target + _VTS_BITNESS_32))
+                targets.add(os.path.join(rel_out_dir, push_target + _VTS_BITNESS_64))
+            else:
+                targets.add(os.path.join(rel_out_dir, push_target))
+        elif name == _VTS_TEST_FILE:
+            # The _VTS_TEST_FILE values can be set in 2 possible ways:
+            #   1. test_file.apk
+            #   2. DATA/app/test_file/test_file.apk
+            # We'll assume that test_file.apk (#1) is in an expected path (but
+            # that is not true, see b/76158619) and create the full path for it
+            # and then append the _VTS_TEST_FILE value to targets to build.
+            target = os.path.join(rel_out_dir, value)
+            # If value is just an APK, specify the path that we expect it to be in
+            # e.g. out/host/linux-x86/vts/android-vts/testcases/DATA/app/test_file/test_file.apk
+            head, _ = os.path.split(value)
+            if not head:
+                target = os.path.join(rel_out_dir, _VTS_OUT_DATA_APP_PATH,
+                                      _get_apk_target(value), value)
+            targets.add(target)
+        elif name == _VTS_APK:
+            targets.add(os.path.join(rel_out_dir, value))
+    logging.debug('Targets found in config file: %s', targets)
+    return targets
+
+
+def get_dir_path_and_filename(path):
+    """Return tuple of dir and file name from given path.
+
+    Args:
+        path: String of path to break up.
+
+    Returns:
+        Tuple of (dir, file) paths.
+    """
+    if os.path.isfile(path):
+        dir_path, file_path = os.path.split(path)
+    else:
+        dir_path, file_path = path, None
+    return dir_path, file_path
+
+
+def get_cc_filter(class_name, methods):
+    """Get the cc filter.
+
+    Args:
+        class_name: class name of the cc test.
+        methods: a list of method names.
+
+    Returns:
+        A formatted string for cc filter.
+        Ex: "class1.method1:class1.method2" or "class1.*"
+    """
+    if methods:
+        return ":".join(["%s.%s" % (class_name, x) for x in methods])
+    return "%s.*" % class_name
+
+
+def search_integration_dirs(name, int_dirs):
+    """Search integration dirs for name and return full path.
+
+    Args:
+        name: A string of plan name needed to be found.
+        int_dirs: A list of path needed to be searched.
+
+    Returns:
+        A list of the test path.
+        Ask user to select if multiple tests are found.
+        None if no matched test found.
+    """
+    root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
+    test_files = []
+    for integration_dir in int_dirs:
+        abs_path = os.path.join(root_dir, integration_dir)
+        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)
+
+
+def get_int_dir_from_path(path, int_dirs):
+    """Search integration dirs for the given path and return path of dir.
+
+    Args:
+        path: A string of path needed to be found.
+        int_dirs: A list of path needed to be searched.
+
+    Returns:
+        A string of the test dir. None if no matched path found.
+    """
+    root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
+    if not os.path.exists(path):
+        return None
+    dir_path, file_name = get_dir_path_and_filename(path)
+    int_dir = None
+    for possible_dir in int_dirs:
+        abs_int_dir = os.path.join(root_dir, possible_dir)
+        if is_equal_or_sub_dir(dir_path, abs_int_dir):
+            int_dir = abs_int_dir
+            break
+    if not file_name:
+        logging.warn('Found dir (%s) matching input (%s).'
+                     ' Referencing an entire Integration/Suite dir'
+                     ' is not supported. If you are trying to reference'
+                     ' a test by its path, please input the path to'
+                     ' the integration/suite config file itself.',
+                     int_dir, path)
+        return None
+    return int_dir
+
+
+def get_install_locations(installed_paths):
+    """Get install locations from installed paths.
+
+    Args:
+        installed_paths: List of installed_paths from module_info.
+
+    Returns:
+        Set of install locations from module_info installed_paths. e.g.
+        set(['host', 'device'])
+    """
+    install_locations = set()
+    for path in installed_paths:
+        if _HOST_PATH_RE.match(path):
+            install_locations.add(constants.DEVICELESS_TEST)
+        elif _DEVICE_PATH_RE.match(path):
+            install_locations.add(constants.DEVICE_TEST)
+    return install_locations
+
+
+def get_levenshtein_distance(test_name, module_name, dir_costs=constants.COST_TYPO):
+    """Return an edit distance between test_name and module_name.
+
+    Levenshtein Distance has 3 actions: delete, insert and replace.
+    dis_costs makes each action weigh differently.
+
+    Args:
+        test_name: A keyword from the users.
+        module_name: A testable module name.
+        dir_costs: A tuple which contains 3 integer, where dir represents
+                   Deletion, Insertion and Replacement respectively.
+                   For guessing typos: (1, 1, 1) gives the best result.
+                   For searching keywords, (8, 1, 5) gives the best result.
+
+    Returns:
+        An edit distance integer between test_name and module_name.
+    """
+    rows = len(test_name) + 1
+    cols = len(module_name) + 1
+    deletion, insertion, replacement = dir_costs
+
+    # Creating a Dynamic Programming Matrix and weighting accordingly.
+    dp_matrix = [[0 for _ in range(cols)] for _ in range(rows)]
+    # Weigh rows/deletion
+    for row in range(1, rows):
+        dp_matrix[row][0] = row * deletion
+    # Weigh cols/insertion
+    for col in range(1, cols):
+        dp_matrix[0][col] = col * insertion
+    # The core logic of LD
+    for col in range(1, cols):
+        for row in range(1, rows):
+            if test_name[row-1] == module_name[col-1]:
+                cost = 0
+            else:
+                cost = replacement
+            dp_matrix[row][col] = min(dp_matrix[row-1][col] + deletion,
+                                      dp_matrix[row][col-1] + insertion,
+                                      dp_matrix[row-1][col-1] + cost)
+
+    return dp_matrix[row][col]
diff --git a/atest/test_finders/test_finder_utils_unittest.py b/atest/test_finders/test_finder_utils_unittest.py
new file mode 100755
index 0000000..db0496b
--- /dev/null
+++ b/atest/test_finders/test_finder_utils_unittest.py
@@ -0,0 +1,585 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for test_finder_utils."""
+
+import os
+import unittest
+import mock
+
+# pylint: disable=import-error
+import atest_error
+import constants
+import module_info
+import unittest_constants as uc
+import unittest_utils
+from test_finders import test_finder_utils
+
+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'
+VTS_PLAN_DIR = 'vts_plan_files'
+VTS_XML_TARGETS = {'VtsTestName',
+                   'DATA/nativetest/vts_treble_vintf_test/vts_treble_vintf_test',
+                   'DATA/nativetest64/vts_treble_vintf_test/vts_treble_vintf_test',
+                   'DATA/lib/libhidl-gen-hash.so',
+                   'DATA/lib64/libhidl-gen-hash.so',
+                   'hal-hidl-hash/frameworks/hardware/interfaces/current.txt',
+                   'hal-hidl-hash/hardware/interfaces/current.txt',
+                   'hal-hidl-hash/system/hardware/interfaces/current.txt',
+                   'hal-hidl-hash/system/libhidl/transport/current.txt',
+                   'target_with_delim',
+                   'out/dir/target',
+                   'push_file1_target1',
+                   'push_file1_target2',
+                   'push_file2_target1',
+                   'push_file2_target2',
+                   'CtsDeviceInfo.apk',
+                   'DATA/app/DeviceHealthTests/DeviceHealthTests.apk',
+                   'DATA/app/sl4a/sl4a.apk'}
+VTS_PLAN_TARGETS = {os.path.join(uc.TEST_DATA_DIR, VTS_PLAN_DIR, 'vts-staging-default.xml'),
+                    os.path.join(uc.TEST_DATA_DIR, VTS_PLAN_DIR, 'vts-aa.xml'),
+                    os.path.join(uc.TEST_DATA_DIR, VTS_PLAN_DIR, 'vts-bb.xml'),
+                    os.path.join(uc.TEST_DATA_DIR, VTS_PLAN_DIR, 'vts-cc.xml'),
+                    os.path.join(uc.TEST_DATA_DIR, VTS_PLAN_DIR, 'vts-dd.xml')}
+XML_TARGETS = {'CtsJankDeviceTestCases', 'perf-setup.sh', 'cts-tradefed',
+               'GtsEmptyTestApp'}
+PATH_TO_MODULE_INFO_WITH_AUTOGEN = {
+    'foo/bar/jank' : [{'auto_test_config' : True}]}
+PATH_TO_MODULE_INFO_WITH_MULTI_AUTOGEN = {
+    'foo/bar/jank' : [{'auto_test_config' : True},
+                      {'auto_test_config' : True}]}
+PATH_TO_MODULE_INFO_WITH_MULTI_AUTOGEN_AND_ROBO = {
+    'foo/bar' : [{'auto_test_config' : True},
+                 {'auto_test_config' : True}],
+    'foo/bar/jank': [{constants.MODULE_CLASS : [constants.MODULE_CLASS_ROBOLECTRIC]}]}
+
+#pylint: disable=protected-access
+class TestFinderUtilsUnittests(unittest.TestCase):
+    """Unit tests for test_finder_utils.py"""
+
+    def test_split_methods(self):
+        """Test _split_methods method."""
+        # Class
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.split_methods('Class.Name'),
+            ('Class.Name', set()))
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.split_methods('Class.Name#Method'),
+            ('Class.Name', {'Method'}))
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.split_methods('Class.Name#Method,Method2'),
+            ('Class.Name', {'Method', 'Method2'}))
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.split_methods('Class.Name#Method,Method2'),
+            ('Class.Name', {'Method', 'Method2'}))
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.split_methods('Class.Name#Method,Method2'),
+            ('Class.Name', {'Method', 'Method2'}))
+        self.assertRaises(
+            atest_error.TooManyMethodsError, test_finder_utils.split_methods,
+            'class.name#Method,class.name.2#method')
+        # Path
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.split_methods('foo/bar/class.java'),
+            ('foo/bar/class.java', set()))
+        unittest_utils.assert_strict_equal(
+            self,
+            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, _, has_method):
+        """Test extract_test_dir method."""
+        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), 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), 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)
+        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), 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), 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), [])
+        mock_input.return_value = 'lOO'
+        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):
+        """Test is_equal_or_sub_dir method."""
+        self.assertTrue(test_finder_utils.is_equal_or_sub_dir('/a/b/c', '/'))
+        self.assertTrue(test_finder_utils.is_equal_or_sub_dir('/a/b/c', '/a'))
+        self.assertTrue(test_finder_utils.is_equal_or_sub_dir('/a/b/c',
+                                                              '/a/b/c'))
+        self.assertFalse(test_finder_utils.is_equal_or_sub_dir('/a/b',
+                                                               '/a/b/c'))
+        self.assertFalse(test_finder_utils.is_equal_or_sub_dir('/a', '/f'))
+        mock_isdir.return_value = False
+        self.assertFalse(test_finder_utils.is_equal_or_sub_dir('/a/b', '/a'))
+
+    @mock.patch('os.path.isdir', return_value=True)
+    @mock.patch('os.path.isfile',
+                side_effect=unittest_utils.isfile_side_effect)
+    def test_find_parent_module_dir(self, _isfile, _isdir):
+        """Test _find_parent_module_dir method."""
+        abs_class_dir = '/%s' % CLASS_DIR
+        mock_module_info = mock.Mock(spec=module_info.ModuleInfo)
+        mock_module_info.path_to_module_info = {}
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.find_parent_module_dir(uc.ROOT,
+                                                     abs_class_dir,
+                                                     mock_module_info),
+            uc.MODULE_DIR)
+
+    @mock.patch('os.path.isdir', return_value=True)
+    @mock.patch('os.path.isfile', return_value=False)
+    def test_find_parent_module_dir_with_autogen_config(self, _isfile, _isdir):
+        """Test _find_parent_module_dir method."""
+        abs_class_dir = '/%s' % CLASS_DIR
+        mock_module_info = mock.Mock(spec=module_info.ModuleInfo)
+        mock_module_info.path_to_module_info = PATH_TO_MODULE_INFO_WITH_AUTOGEN
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.find_parent_module_dir(uc.ROOT,
+                                                     abs_class_dir,
+                                                     mock_module_info),
+            uc.MODULE_DIR)
+
+    @mock.patch('os.path.isdir', return_value=True)
+    @mock.patch('os.path.isfile', side_effect=[False] * 5 + [True])
+    def test_find_parent_module_dir_with_autogen_subconfig(self, _isfile, _isdir):
+        """Test _find_parent_module_dir method.
+
+        This case is testing when the auto generated config is in a
+        sub-directory of a larger test that contains a test config in a parent
+        directory.
+        """
+        abs_class_dir = '/%s' % CLASS_DIR
+        mock_module_info = mock.Mock(spec=module_info.ModuleInfo)
+        mock_module_info.path_to_module_info = (
+            PATH_TO_MODULE_INFO_WITH_MULTI_AUTOGEN)
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.find_parent_module_dir(uc.ROOT,
+                                                     abs_class_dir,
+                                                     mock_module_info),
+            uc.MODULE_DIR)
+
+    @mock.patch('os.path.isdir', return_value=True)
+    @mock.patch('os.path.isfile', return_value=False)
+    def test_find_parent_module_dir_with_multi_autogens(self, _isfile, _isdir):
+        """Test _find_parent_module_dir method.
+
+        This case returns folders with multiple autogenerated configs defined.
+        """
+        abs_class_dir = '/%s' % CLASS_DIR
+        mock_module_info = mock.Mock(spec=module_info.ModuleInfo)
+        mock_module_info.path_to_module_info = (
+            PATH_TO_MODULE_INFO_WITH_MULTI_AUTOGEN)
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.find_parent_module_dir(uc.ROOT,
+                                                     abs_class_dir,
+                                                     mock_module_info),
+            uc.MODULE_DIR)
+
+    @mock.patch('os.path.isdir', return_value=True)
+    @mock.patch('os.path.isfile', return_value=False)
+    def test_find_parent_module_dir_with_robo_and_autogens(self, _isfile,
+                                                           _isdir):
+        """Test _find_parent_module_dir method.
+
+        This case returns folders with multiple autogenerated configs defined
+        with a Robo test above them, which is the expected result.
+        """
+        abs_class_dir = '/%s' % CLASS_DIR
+        mock_module_info = mock.Mock(spec=module_info.ModuleInfo)
+        mock_module_info.path_to_module_info = (
+            PATH_TO_MODULE_INFO_WITH_MULTI_AUTOGEN_AND_ROBO)
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.find_parent_module_dir(uc.ROOT,
+                                                     abs_class_dir,
+                                                     mock_module_info),
+            uc.MODULE_DIR)
+
+
+    @mock.patch('os.path.isdir', return_value=True)
+    @mock.patch('os.path.isfile', return_value=False)
+    def test_find_parent_module_dir_robo(self, _isfile, _isdir):
+        """Test _find_parent_module_dir method.
+
+        Make sure we behave as expected when we encounter a robo module path.
+        """
+        abs_class_dir = '/%s' % CLASS_DIR
+        mock_module_info = mock.Mock(spec=module_info.ModuleInfo)
+        mock_module_info.is_robolectric_module.return_value = True
+        rel_class_dir_path = os.path.relpath(abs_class_dir, uc.ROOT)
+        mock_module_info.path_to_module_info = {rel_class_dir_path: [{}]}
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.find_parent_module_dir(uc.ROOT,
+                                                     abs_class_dir,
+                                                     mock_module_info),
+            rel_class_dir_path)
+
+    def test_get_targets_from_xml(self):
+        """Test get_targets_from_xml method."""
+        # Mocking Etree is near impossible, so use a real file, but mocking
+        # ModuleInfo is still fine. Just have it return False when it finds a
+        # module that states it's not a module.
+        mock_module_info = mock.Mock(spec=module_info.ModuleInfo)
+        mock_module_info.is_module.side_effect = lambda module: (
+            not module == 'is_not_module')
+        xml_file = os.path.join(uc.TEST_DATA_DIR, constants.MODULE_CONFIG)
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.get_targets_from_xml(xml_file, mock_module_info),
+            XML_TARGETS)
+
+    @mock.patch.object(test_finder_utils, '_VTS_PUSH_DIR',
+                       os.path.join(uc.TEST_DATA_DIR, VTS_PUSH_DIR))
+    def test_get_targets_from_vts_xml(self):
+        """Test get_targets_from_xml method."""
+        # Mocking Etree is near impossible, so use a real file, but mock out
+        # ModuleInfo,
+        mock_module_info = mock.Mock(spec=module_info.ModuleInfo)
+        mock_module_info.is_module.return_value = True
+        xml_file = os.path.join(uc.TEST_DATA_DIR, VTS_XML)
+        unittest_utils.assert_strict_equal(
+            self,
+            test_finder_utils.get_targets_from_vts_xml(xml_file, '',
+                                                       mock_module_info),
+            VTS_XML_TARGETS)
+
+    @mock.patch('subprocess.check_output')
+    def test_get_ignored_dirs(self, _mock_check_output):
+        """Test _get_ignored_dirs method."""
+
+        # Clean cached value for test.
+        test_finder_utils._get_ignored_dirs.cached_ignore_dirs = []
+
+        build_top = '/a/b'
+        _mock_check_output.return_value = ('/a/b/c/.find-ignore\n'
+                                           '/a/b/out/.out-dir\n'
+                                           '/a/b/d/.out-dir\n\n')
+        # Case 1: $OUT_DIR = ''. No customized out dir.
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: build_top,
+                           constants.ANDROID_OUT_DIR: ''}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            correct_ignore_dirs = ['/a/b/c', '/a/b/out', '/a/b/d']
+            ignore_dirs = test_finder_utils._get_ignored_dirs()
+            self.assertEqual(ignore_dirs, correct_ignore_dirs)
+        # Case 2: $OUT_DIR = 'out2'
+        test_finder_utils._get_ignored_dirs.cached_ignore_dirs = []
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: build_top,
+                           constants.ANDROID_OUT_DIR: 'out2'}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            correct_ignore_dirs = ['/a/b/c', '/a/b/out', '/a/b/d', '/a/b/out2']
+            ignore_dirs = test_finder_utils._get_ignored_dirs()
+            self.assertEqual(ignore_dirs, correct_ignore_dirs)
+        # Case 3: The $OUT_DIR is abs dir but not under $ANDROID_BUILD_TOP
+        test_finder_utils._get_ignored_dirs.cached_ignore_dirs = []
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: build_top,
+                           constants.ANDROID_OUT_DIR: '/x/y/e/g'}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            correct_ignore_dirs = ['/a/b/c', '/a/b/out', '/a/b/d']
+            ignore_dirs = test_finder_utils._get_ignored_dirs()
+            self.assertEqual(ignore_dirs, correct_ignore_dirs)
+        # Case 4: The $OUT_DIR is abs dir and under $ANDROID_BUILD_TOP
+        test_finder_utils._get_ignored_dirs.cached_ignore_dirs = []
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: build_top,
+                           constants.ANDROID_OUT_DIR: '/a/b/e/g'}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            correct_ignore_dirs = ['/a/b/c', '/a/b/out', '/a/b/d', '/a/b/e/g']
+            ignore_dirs = test_finder_utils._get_ignored_dirs()
+            self.assertEqual(ignore_dirs, correct_ignore_dirs)
+        # Case 5: There is a file of '.out-dir' under $OUT_DIR.
+        test_finder_utils._get_ignored_dirs.cached_ignore_dirs = []
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: build_top,
+                           constants.ANDROID_OUT_DIR: 'out'}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            correct_ignore_dirs = ['/a/b/c', '/a/b/out', '/a/b/d']
+            ignore_dirs = test_finder_utils._get_ignored_dirs()
+            self.assertEqual(ignore_dirs, correct_ignore_dirs)
+        # Case 6: Testing cache. All of the changes are useless.
+        _mock_check_output.return_value = ('/a/b/X/.find-ignore\n'
+                                           '/a/b/YY/.out-dir\n'
+                                           '/a/b/d/.out-dir\n\n')
+        os_environ_mock = {constants.ANDROID_BUILD_TOP: build_top,
+                           constants.ANDROID_OUT_DIR: 'new'}
+        with mock.patch.dict('os.environ', os_environ_mock, clear=True):
+            cached_answer = ['/a/b/c', '/a/b/out', '/a/b/d']
+            none_cached_answer = ['/a/b/X', '/a/b/YY', '/a/b/d', 'a/b/new']
+            ignore_dirs = test_finder_utils._get_ignored_dirs()
+            self.assertEqual(ignore_dirs, cached_answer)
+            self.assertNotEqual(ignore_dirs, none_cached_answer)
+
+    @mock.patch('__builtin__.raw_input', return_value='0')
+    def test_search_integration_dirs(self, mock_input):
+        """Test search_integration_dirs."""
+        mock_input.return_value = '0'
+        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, 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, paths)
+
+    @mock.patch('os.path.isfile', return_value=False)
+    @mock.patch('os.environ.get', return_value=uc.TEST_CONFIG_DATA_DIR)
+    @mock.patch('__builtin__.raw_input', return_value='0')
+    # pylint: disable=too-many-statements
+    def test_find_class_file(self, mock_input, _mock_env, _mock_isfile):
+        """Test find_class_file."""
+        # 1. Java class(find).
+        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.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.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
+                                                                      uc.FIND_PATH_TESTCASE_JAVA))
+        self.assertTrue(java_class in java_tmp_test_result)
+        self.assertTrue(kt_class in java_tmp_test_result)
+
+        # 2. Java class(read index).
+        del java_tmp_test_result[:]
+        mock_input.return_value = '0'
+        _mock_isfile = True
+        test_finder_utils.FIND_INDEXES['CLASS'] = uc.CLASS_INDEX
+        java_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_TESTCASE_JAVA + '.java')
+        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.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
+                                                                      uc.FIND_PATH_TESTCASE_JAVA))
+        self.assertTrue(java_class in java_tmp_test_result)
+        self.assertTrue(kt_class in java_tmp_test_result)
+
+        # 3. Qualified Java class(find).
+        del java_tmp_test_result[:]
+        mock_input.return_value = '0'
+        _mock_isfile = False
+        java_qualified_class = '{0}.{1}'.format(uc.FIND_PATH_FOLDER, uc.FIND_PATH_TESTCASE_JAVA)
+        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.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)
+
+        # 4. Qualified Java class(read index).
+        del java_tmp_test_result[:]
+        mock_input.return_value = '0'
+        _mock_isfile = True
+        test_finder_utils.FIND_INDEXES['QUALIFIED_CLASS'] = uc.QCLASS_INDEX
+        java_qualified_class = '{0}.{1}'.format(uc.FIND_PATH_FOLDER, uc.FIND_PATH_TESTCASE_JAVA)
+        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.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)
+
+        # 5. CC class(find).
+        cc_tmp_test_result = []
+        _mock_isfile = False
+        mock_input.return_value = '0'
+        cpp_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_FILENAME_CC + '.cpp')
+        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.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
+                                                                    uc.FIND_PATH_TESTCASE_CC,
+                                                                    True))
+        self.assertTrue(cpp_class in cc_tmp_test_result)
+        self.assertTrue(cc_class in cc_tmp_test_result)
+
+        # 6. CC class(read index).
+        del cc_tmp_test_result[:]
+        mock_input.return_value = '0'
+        _mock_isfile = True
+        test_finder_utils.FIND_INDEXES['CC_CLASS'] = uc.CC_CLASS_INDEX
+        cpp_class = os.path.join(uc.FIND_PATH, uc.FIND_PATH_FILENAME_CC + '.cpp')
+        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.extend(test_finder_utils.find_class_file(uc.FIND_PATH,
+                                                                    uc.FIND_PATH_TESTCASE_CC,
+                                                                    True))
+        self.assertTrue(cpp_class in cc_tmp_test_result)
+        self.assertTrue(cc_class in cc_tmp_test_result)
+
+    @mock.patch('__builtin__.raw_input', return_value='0')
+    @mock.patch.object(test_finder_utils, 'get_dir_path_and_filename')
+    @mock.patch('os.path.exists', return_value=True)
+    def test_get_int_dir_from_path(self, _exists, _find, mock_input):
+        """Test get_int_dir_from_path."""
+        mock_input.return_value = '0'
+        int_dirs = [INT_DIR1]
+        path = os.path.join(uc.ROOT, INT_DIR1, INT_FILE_NAME+'.xml')
+        _find.return_value = (INT_DIR1, INT_FILE_NAME+'.xml')
+        test_result = test_finder_utils.get_int_dir_from_path(path, int_dirs)
+        unittest_utils.assert_strict_equal(self, test_result, INT_DIR1)
+        _find.return_value = (INT_DIR1, None)
+        test_result = test_finder_utils.get_int_dir_from_path(path, int_dirs)
+        unittest_utils.assert_strict_equal(self, test_result, None)
+        int_dirs = [INT_DIR1, INT_DIR2]
+        _find.return_value = (INT_DIR1, INT_FILE_NAME+'.xml')
+        test_result = test_finder_utils.get_int_dir_from_path(path, int_dirs)
+        unittest_utils.assert_strict_equal(self, test_result, INT_DIR1)
+
+    def test_get_install_locations(self):
+        """Test get_install_locations."""
+        host_installed_paths = ["out/host/a/b"]
+        host_expect = set(['host'])
+        self.assertEqual(test_finder_utils.get_install_locations(host_installed_paths),
+                         host_expect)
+        device_installed_paths = ["out/target/c/d"]
+        device_expect = set(['device'])
+        self.assertEqual(test_finder_utils.get_install_locations(device_installed_paths),
+                         device_expect)
+        both_installed_paths = ["out/host/e", "out/target/f"]
+        both_expect = set(['host', 'device'])
+        self.assertEqual(test_finder_utils.get_install_locations(both_installed_paths),
+                         both_expect)
+        no_installed_paths = []
+        no_expect = set()
+        self.assertEqual(test_finder_utils.get_install_locations(no_installed_paths),
+                         no_expect)
+
+    def test_get_plans_from_vts_xml(self):
+        """Test get_plans_from_vts_xml method."""
+        xml_path = os.path.join(uc.TEST_DATA_DIR, VTS_PLAN_DIR, 'vts-staging-default.xml')
+        self.assertEqual(
+            test_finder_utils.get_plans_from_vts_xml(xml_path),
+            VTS_PLAN_TARGETS)
+        xml_path = os.path.join(uc.TEST_DATA_DIR, VTS_PLAN_DIR, 'NotExist.xml')
+        self.assertRaises(atest_error.XmlNotExistError,
+                          test_finder_utils.get_plans_from_vts_xml, xml_path)
+
+    def test_get_levenshtein_distance(self):
+        """Test get_levenshetine distance module correctly returns distance."""
+        self.assertEqual(test_finder_utils.get_levenshtein_distance(uc.MOD1, uc.FUZZY_MOD1), 1)
+        self.assertEqual(test_finder_utils.get_levenshtein_distance(uc.MOD2, uc.FUZZY_MOD2,
+                                                                    dir_costs=(1, 2, 3)), 3)
+        self.assertEqual(test_finder_utils.get_levenshtein_distance(uc.MOD3, uc.FUZZY_MOD3,
+                                                                    dir_costs=(1, 2, 1)), 8)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_finders/test_info.py b/atest/test_finders/test_info.py
new file mode 100644
index 0000000..d872576
--- /dev/null
+++ b/atest/test_finders/test_info.py
@@ -0,0 +1,120 @@
+# Copyright 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.
+
+"""
+TestInfo class.
+"""
+
+from collections import namedtuple
+
+# pylint: disable=import-error
+import constants
+
+
+TestFilterBase = namedtuple('TestFilter', ['class_name', 'methods'])
+
+
+class TestInfo(object):
+    """Information needed to identify and run a test."""
+
+    # pylint: disable=too-many-arguments
+    def __init__(self, test_name, test_runner, build_targets, data=None,
+                 suite=None, module_class=None, install_locations=None,
+                 test_finder='', compatibility_suites=None):
+        """Init for TestInfo.
+
+        Args:
+            test_name: String of test name.
+            test_runner: String of test runner.
+            build_targets: Set of build targets.
+            data: Dict of data for test runners to use.
+            suite: Suite for test runners to use.
+            module_class: A list of test classes. It's a snippet of class
+                        in module_info. e.g. ["EXECUTABLES",  "NATIVE_TESTS"]
+            install_locations: Set of install locations.
+                        e.g. set(['host', 'device'])
+            test_finder: String of test finder.
+            compatibility_suites: A list of compatibility_suites. It's a
+                        snippet of compatibility_suites in module_info. e.g.
+                        ["device-tests",  "vts-core"]
+        """
+        self.test_name = test_name
+        self.test_runner = test_runner
+        self.build_targets = build_targets
+        self.data = data if data else {}
+        self.suite = suite
+        self.module_class = module_class if module_class else []
+        self.install_locations = (install_locations if install_locations
+                                  else set())
+        # True if the TestInfo is built from a test configured in TEST_MAPPING.
+        self.from_test_mapping = False
+        # True if the test should run on host and require no device. The
+        # attribute is only set through TEST_MAPPING file.
+        self.host = False
+        self.test_finder = test_finder
+        self.compatibility_suites = (map(str, compatibility_suites)
+                                     if compatibility_suites else [])
+
+    def __str__(self):
+        host_info = (' - runs on host without device required.' if self.host
+                     else '')
+        return ('test_name: %s - test_runner:%s - build_targets:%s - data:%s - '
+                'suite:%s - module_class: %s - install_locations:%s%s - '
+                'test_finder: %s - compatibility_suites:%s' % (
+                    self.test_name, self.test_runner, self.build_targets,
+                    self.data, self.suite, self.module_class,
+                    self.install_locations, host_info, self.test_finder,
+                    self.compatibility_suites))
+
+    def get_supported_exec_mode(self):
+        """Get the supported execution mode of the test.
+
+        Determine the test supports which execution mode by strategy:
+        Robolectric/JAVA_LIBRARIES --> 'both'
+        Not native tests or installed only in out/target --> 'device'
+        Installed only in out/host --> 'both'
+        Installed under host and target --> 'both'
+
+        Return:
+            String of execution mode.
+        """
+        install_path = self.install_locations
+        if not self.module_class:
+            return constants.DEVICE_TEST
+        # Let Robolectric test support both.
+        if constants.MODULE_CLASS_ROBOLECTRIC in self.module_class:
+            return constants.BOTH_TEST
+        # Let JAVA_LIBRARIES support both.
+        if constants.MODULE_CLASS_JAVA_LIBRARIES in self.module_class:
+            return constants.BOTH_TEST
+        if not install_path:
+            return constants.DEVICE_TEST
+        # Non-Native test runs on device-only.
+        if constants.MODULE_CLASS_NATIVE_TESTS not in self.module_class:
+            return constants.DEVICE_TEST
+        # Native test with install path as host should be treated as both.
+        # Otherwise, return device test.
+        if len(install_path) == 1 and constants.DEVICE_TEST in install_path:
+            return constants.DEVICE_TEST
+        return constants.BOTH_TEST
+
+
+class TestFilter(TestFilterBase):
+    """Information needed to filter a test in Tradefed"""
+
+    def to_set_of_tf_strings(self):
+        """Return TestFilter as set of strings in TradeFed filter format."""
+        if self.methods:
+            return {'%s#%s' % (self.class_name, m) for m in self.methods}
+        return {self.class_name}
diff --git a/atest/test_finders/tf_integration_finder.py b/atest/test_finders/tf_integration_finder.py
new file mode 100644
index 0000000..eecb992
--- /dev/null
+++ b/atest/test_finders/tf_integration_finder.py
@@ -0,0 +1,269 @@
+# Copyright 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.
+
+"""
+Integration Finder class.
+"""
+
+import copy
+import logging
+import os
+import re
+import xml.etree.ElementTree as ElementTree
+
+# pylint: disable=import-error
+import atest_error
+import constants
+from test_finders import test_info
+from test_finders import test_finder_base
+from test_finders import test_finder_utils
+from test_runners import atest_tf_test_runner
+
+# Find integration name based on file path of integration config xml file.
+# Group matches "foo/bar" given "blah/res/config/blah/res/config/foo/bar.xml
+_INT_NAME_RE = re.compile(r'^.*\/res\/config\/(?P<int_name>.*).xml$')
+_TF_TARGETS = frozenset(['tradefed', 'tradefed-contrib'])
+_GTF_TARGETS = frozenset(['google-tradefed', 'google-tradefed-contrib'])
+_CONTRIB_TARGETS = frozenset(['tradefed-contrib', 'google-tradefed-contrib'])
+_TF_RES_DIR = '../res/config'
+
+
+class TFIntegrationFinder(test_finder_base.TestFinderBase):
+    """Integration Finder class."""
+    NAME = 'INTEGRATION'
+    _TEST_RUNNER = atest_tf_test_runner.AtestTradefedTestRunner.NAME
+
+
+    def __init__(self, module_info=None):
+        super(TFIntegrationFinder, self).__init__()
+        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
+        self.module_info = module_info
+        # TODO: Break this up into AOSP/google_tf integration finders.
+        self.tf_dirs, self.gtf_dirs = self._get_integration_dirs()
+        self.integration_dirs = self.tf_dirs + self.gtf_dirs
+
+    def _get_mod_paths(self, module_name):
+        """Return the paths of the given module name."""
+        if self.module_info:
+            # Since aosp/801774 merged, the path of test configs have been
+            # changed to ../res/config.
+            if module_name in _CONTRIB_TARGETS:
+                mod_paths = self.module_info.get_paths(module_name)
+                return [os.path.join(path, _TF_RES_DIR) for path in mod_paths]
+            return self.module_info.get_paths(module_name)
+        return []
+
+    def _get_integration_dirs(self):
+        """Get integration dirs from MODULE_INFO based on targets.
+
+        Returns:
+            A tuple of lists of strings of integration dir rel to repo root.
+        """
+        tf_dirs = filter(None, [d for x in _TF_TARGETS for d in self._get_mod_paths(x)])
+        gtf_dirs = filter(None, [d for x in _GTF_TARGETS for d in self._get_mod_paths(x)])
+        return tf_dirs, gtf_dirs
+
+    def _get_build_targets(self, rel_config):
+        config_file = os.path.join(self.root_dir, rel_config)
+        xml_root = self._load_xml_file(config_file)
+        targets = test_finder_utils.get_targets_from_xml_root(xml_root,
+                                                              self.module_info)
+        if self.gtf_dirs:
+            targets.add(constants.GTF_TARGET)
+        return frozenset(targets)
+
+    def _load_xml_file(self, path):
+        """Load an xml file with option to expand <include> tags
+
+        Args:
+            path: A string of path to xml file.
+
+        Returns:
+            An xml.etree.ElementTree.Element instance of the root of the tree.
+        """
+        tree = ElementTree.parse(path)
+        root = tree.getroot()
+        self._load_include_tags(root)
+        return root
+
+    #pylint: disable=invalid-name
+    def _load_include_tags(self, root):
+        """Recursively expand in-place the <include> tags in a given xml tree.
+
+        Python xml libraries don't support our type of <include> tags. Logic used
+        below is modified version of the built-in ElementInclude logic found here:
+        https://github.com/python/cpython/blob/2.7/Lib/xml/etree/ElementInclude.py
+
+        Args:
+            root: The root xml.etree.ElementTree.Element.
+
+        Returns:
+            An xml.etree.ElementTree.Element instance with include tags expanded
+        """
+        i = 0
+        while i < len(root):
+            elem = root[i]
+            if elem.tag == 'include':
+                # expand included xml file
+                integration_name = elem.get('name')
+                if not integration_name:
+                    logging.warn('skipping <include> tag with no "name" value')
+                    continue
+                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)
+                node = copy.copy(node)
+                if elem.tail:
+                    node.tail = (node.tail or "") + elem.tail
+                root[i] = node
+            i = i + 1
+
+    def _search_integration_dirs(self, name):
+        """Search integration dirs for name and return full path.
+        Args:
+            name: A string of integration name as seen in tf's list configs.
+
+        Returns:
+            A list of test path.
+        """
+        test_files = []
+        for integration_dir in self.integration_dirs:
+            abs_path = os.path.join(self.root_dir, integration_dir)
+            found_test_files = test_finder_utils.run_find_cmd(
+                test_finder_utils.FIND_REFERENCE_TYPE.INTEGRATION,
+                abs_path, name)
+            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.
+
+        Args:
+            name: A string of integration name as seen in tf's list configs.
+
+        Returns:
+            A populated TestInfo namedtuple if test found, else None
+        """
+        class_name = None
+        if ':' in name:
+            name, class_name = name.split(':')
+        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_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.
+
+        Args:
+            name: A string of integration name as seen in tf's list configs.
+            test_file: A string of test_file full path.
+            class_name: A string of user's input.
+
+        Returns:
+            A populated TestInfo namedtuple if test found, else None.
+        """
+        match = _INT_NAME_RE.match(test_file)
+        if not match:
+            logging.error('Integration test outside config dir: %s',
+                          test_file)
+            return None
+        int_name = match.group('int_name')
+        if int_name != name:
+            logging.warn('Input (%s) not valid integration name, '
+                         'did you mean: %s?', name, int_name)
+            return None
+        rel_config = os.path.relpath(test_file, self.root_dir)
+        filters = frozenset()
+        if class_name:
+            class_name, methods = test_finder_utils.split_methods(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)
+                paths = test_finder_utils.find_class_file(self.root_dir,
+                                                          class_name)
+                if not paths:
+                    return None
+                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,
+            build_targets=self._get_build_targets(rel_config),
+            data={constants.TI_REL_CONFIG: rel_config,
+                  constants.TI_FILTER: filters})
+
+    def find_int_test_by_path(self, path):
+        """Find the first test info matching the given path.
+
+        Strategy:
+            path_to_integration_file --> Resolve to INTEGRATION
+            # If the path is a dir, we return nothing.
+            path_to_dir_with_integration_files --> Return None
+
+        Args:
+            path: A string of the test's path.
+
+        Returns:
+            A list of populated TestInfo namedtuple if test found, else None
+        """
+        path, _ = test_finder_utils.split_methods(path)
+
+        # Make sure we're looking for a config.
+        if not path.endswith('.xml'):
+            return None
+
+        # TODO: See if this can be generalized and shared with methods above
+        # create absolute path from cwd and remove symbolic links
+        path = os.path.realpath(path)
+        if not os.path.exists(path):
+            return None
+        int_dir = test_finder_utils.get_int_dir_from_path(path,
+                                                          self.integration_dirs)
+        if int_dir:
+            rel_config = os.path.relpath(path, self.root_dir)
+            match = _INT_NAME_RE.match(rel_config)
+            if not match:
+                logging.error('Integration test outside config dir: %s',
+                              rel_config)
+                return None
+            int_name = match.group('int_name')
+            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()})]
+        return None
diff --git a/atest/test_finders/tf_integration_finder_unittest.py b/atest/test_finders/tf_integration_finder_unittest.py
new file mode 100755
index 0000000..170da0c
--- /dev/null
+++ b/atest/test_finders/tf_integration_finder_unittest.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for tf_integration_finder."""
+
+import os
+import unittest
+import mock
+
+# pylint: disable=import-error
+import constants
+import unittest_constants as uc
+import unittest_utils
+from test_finders import test_finder_utils
+from test_finders import test_info
+from test_finders import tf_integration_finder
+from test_runners import atest_tf_test_runner as atf_tr
+
+
+INT_NAME_CLASS = uc.INT_NAME + ':' + uc.FULL_CLASS_NAME
+INT_NAME_METHOD = INT_NAME_CLASS + '#' + uc.METHOD_NAME
+GTF_INT_CONFIG = os.path.join(uc.GTF_INT_DIR, uc.GTF_INT_NAME + '.xml')
+INT_CLASS_INFO = test_info.TestInfo(
+    uc.INT_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    set(),
+    data={constants.TI_FILTER: frozenset([uc.CLASS_FILTER]),
+          constants.TI_REL_CONFIG: uc.INT_CONFIG})
+INT_METHOD_INFO = test_info.TestInfo(
+    uc.INT_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    set(),
+    data={constants.TI_FILTER: frozenset([uc.METHOD_FILTER]),
+          constants.TI_REL_CONFIG: uc.INT_CONFIG})
+
+
+class TFIntegrationFinderUnittests(unittest.TestCase):
+    """Unit tests for tf_integration_finder.py"""
+
+    def setUp(self):
+        """Set up for testing."""
+        self.tf_finder = tf_integration_finder.TFIntegrationFinder()
+        self.tf_finder.integration_dirs = [os.path.join(uc.ROOT, uc.INT_DIR),
+                                           os.path.join(uc.ROOT, uc.GTF_INT_DIR)]
+        self.tf_finder.root_dir = uc.ROOT
+
+    @mock.patch.object(tf_integration_finder.TFIntegrationFinder,
+                       '_get_build_targets', return_value=set())
+    @mock.patch.object(test_finder_utils, 'get_fully_qualified_class_name',
+                       return_value=uc.FULL_CLASS_NAME)
+    @mock.patch('subprocess.check_output')
+    @mock.patch('os.path.exists', return_value=True)
+    @mock.patch('os.path.isfile', return_value=False)
+    @mock.patch('os.path.isdir', return_value=False)
+    #pylint: disable=unused-argument
+    def test_find_test_by_integration_name(self, _isdir, _isfile, _path, mock_find,
+                                           _fcqn, _build):
+        """Test find_test_by_integration_name.
+
+        Note that _isfile is always False since we don't index integration tests.
+        """
+        mock_find.return_value = os.path.join(uc.ROOT, uc.INT_DIR, uc.INT_NAME + '.xml')
+        t_infos = self.tf_finder.find_test_by_integration_name(uc.INT_NAME)
+        self.assertEqual(len(t_infos), 0)
+        _isdir.return_value = True
+        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_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,
+            t_infos[0],
+            uc.GTF_INT_INFO)
+        mock_find.return_value = ''
+        self.assertEqual(
+            self.tf_finder.find_test_by_integration_name('NotIntName'), [])
+
+    @mock.patch.object(tf_integration_finder.TFIntegrationFinder,
+                       '_get_build_targets', return_value=set())
+    @mock.patch('os.path.realpath',
+                side_effect=unittest_utils.realpath_side_effect)
+    @mock.patch('os.path.isdir', return_value=True)
+    @mock.patch('os.path.isfile', return_value=True)
+    @mock.patch.object(test_finder_utils, 'find_parent_module_dir')
+    @mock.patch('os.path.exists', return_value=True)
+    def test_find_int_test_by_path(self, _exists, _find, _isfile, _isdir, _real,
+                                   _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, 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, 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')]
+        xml_file = os.path.join(uc.TEST_DATA_DIR, constants.MODULE_CONFIG)
+        xml_root = self.tf_finder._load_xml_file(xml_file)
+        include_tags = xml_root.findall('.//include')
+        self.assertEqual(0, len(include_tags))
+        option_tags = xml_root.findall('.//option')
+        included = False
+        for tag in option_tags:
+            if tag.attrib['value'].strip() == 'CtsUiDeviceTestCases.apk':
+                included = True
+        self.assertTrue(included)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_mapping.py b/atest/test_mapping.py
new file mode 100644
index 0000000..255e813
--- /dev/null
+++ b/atest/test_mapping.py
@@ -0,0 +1,115 @@
+# Copyright 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.
+
+"""
+Classes for test mapping related objects
+"""
+
+
+import copy
+import os
+
+import constants
+
+
+class TestDetail(object):
+    """Stores the test details set in a TEST_MAPPING file."""
+
+    def __init__(self, details):
+        """TestDetail constructor
+
+        Parse test detail from a dictionary, e.g.,
+        {
+          "name": "SettingsUnitTests",
+          "host": true,
+          "options": [
+            {
+              "instrumentation-arg":
+                  "annotation=android.platform.test.annotations.Presubmit"
+            }
+          ]
+        }
+
+        Args:
+            details: A dictionary of test detail.
+        """
+        self.name = details['name']
+        self.options = []
+        # True if the test should run on host and require no device.
+        self.host = details.get('host', False)
+        assert isinstance(self.host, bool), 'host can only have boolean value.'
+        options = details.get('options', [])
+        for option in options:
+            assert len(option) == 1, 'Each option can only have one key.'
+            self.options.append(copy.deepcopy(option).popitem())
+        self.options.sort(key=lambda o: o[0])
+
+    def __str__(self):
+        """String value of the TestDetail object."""
+        host_info = (', runs on host without device required.' if self.host
+                     else '')
+        if not self.options:
+            return self.name + host_info
+        options = ''
+        for option in self.options:
+            options += '%s: %s, ' % option
+
+        return '%s (%s)%s' % (self.name, options.strip(', '), host_info)
+
+    def __hash__(self):
+        """Get the hash of TestDetail based on the details"""
+        return hash(str(self))
+
+    def __eq__(self, other):
+        return str(self) == str(other)
+
+
+class Import(object):
+    """Store test mapping import details."""
+
+    def __init__(self, test_mapping_file, details):
+        """Import constructor
+
+        Parse import details from a dictionary, e.g.,
+        {
+            "path": "..\folder1"
+        }
+        in which, project is the name of the project, by default it's the
+        current project of the containing TEST_MAPPING file.
+
+        Args:
+            test_mapping_file: Path to the TEST_MAPPING file that contains the
+                import.
+            details: A dictionary of details about importing another
+                TEST_MAPPING file.
+        """
+        self.test_mapping_file = test_mapping_file
+        self.path = details['path']
+
+    def __str__(self):
+        """String value of the Import object."""
+        return 'Source: %s, path: %s' % (self.test_mapping_file, self.path)
+
+    def get_path(self):
+        """Get the path to TEST_MAPPING import directory."""
+        path = os.path.realpath(os.path.join(
+            os.path.dirname(self.test_mapping_file), self.path))
+        if os.path.exists(path):
+            return path
+        root_dir = os.environ.get(constants.ANDROID_BUILD_TOP, os.sep)
+        path = os.path.realpath(os.path.join(root_dir, self.path))
+        if os.path.exists(path):
+            return path
+        # The import path can't be located.
+        return None
diff --git a/atest/test_mapping_unittest.py b/atest/test_mapping_unittest.py
new file mode 100755
index 0000000..55cd839
--- /dev/null
+++ b/atest/test_mapping_unittest.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for test_mapping"""
+
+import unittest
+
+import test_mapping
+import unittest_constants as uc
+
+
+class TestMappingUnittests(unittest.TestCase):
+    """Unit tests for test_mapping.py"""
+
+    def test_parsing(self):
+        """Test creating TestDetail object"""
+        detail = test_mapping.TestDetail(uc.TEST_MAPPING_TEST)
+        self.assertEqual(uc.TEST_MAPPING_TEST['name'], detail.name)
+        self.assertTrue(detail.host)
+        self.assertEqual([], detail.options)
+
+    def test_parsing_with_option(self):
+        """Test creating TestDetail object with option configured"""
+        detail = test_mapping.TestDetail(uc.TEST_MAPPING_TEST_WITH_OPTION)
+        self.assertEqual(uc.TEST_MAPPING_TEST_WITH_OPTION['name'], detail.name)
+        self.assertEqual(uc.TEST_MAPPING_TEST_WITH_OPTION_STR, str(detail))
+
+    def test_parsing_with_bad_option(self):
+        """Test creating TestDetail object with bad option configured"""
+        with self.assertRaises(Exception) as context:
+            test_mapping.TestDetail(uc.TEST_MAPPING_TEST_WITH_BAD_OPTION)
+        self.assertEqual(
+            'Each option can only have one key.', str(context.exception))
+
+    def test_parsing_with_bad_host_value(self):
+        """Test creating TestDetail object with bad host value configured"""
+        with self.assertRaises(Exception) as context:
+            test_mapping.TestDetail(uc.TEST_MAPPING_TEST_WITH_BAD_HOST_VALUE)
+        self.assertEqual(
+            'host can only have boolean value.', str(context.exception))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_runner_handler.py b/atest/test_runner_handler.py
new file mode 100644
index 0000000..86f42cb
--- /dev/null
+++ b/atest/test_runner_handler.py
@@ -0,0 +1,146 @@
+# Copyright 2017, 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.
+
+"""
+Aggregates test runners, groups tests by test runners and kicks off tests.
+"""
+
+import itertools
+import time
+import traceback
+
+import atest_error
+import constants
+import result_reporter
+
+from metrics import metrics
+from metrics import metrics_utils
+from test_runners import atest_tf_test_runner
+from test_runners import robolectric_test_runner
+from test_runners import suite_plan_test_runner
+from test_runners import vts_tf_test_runner
+
+# pylint: disable=line-too-long
+_TEST_RUNNERS = {
+    atest_tf_test_runner.AtestTradefedTestRunner.NAME: atest_tf_test_runner.AtestTradefedTestRunner,
+    robolectric_test_runner.RobolectricTestRunner.NAME: robolectric_test_runner.RobolectricTestRunner,
+    suite_plan_test_runner.SuitePlanTestRunner.NAME: suite_plan_test_runner.SuitePlanTestRunner,
+    vts_tf_test_runner.VtsTradefedTestRunner.NAME: vts_tf_test_runner.VtsTradefedTestRunner,
+}
+
+
+def _get_test_runners():
+    """Returns the test runners.
+
+    If external test runners are defined outside atest, they can be try-except
+    imported into here.
+
+    Returns:
+        Dict of test runner name to test runner class.
+    """
+    test_runners_dict = _TEST_RUNNERS
+    # Example import of example test runner:
+    try:
+        # pylint: disable=line-too-long
+        from test_runners import example_test_runner
+        test_runners_dict[example_test_runner.ExampleTestRunner.NAME] = example_test_runner.ExampleTestRunner
+    except ImportError:
+        pass
+    return test_runners_dict
+
+
+def group_tests_by_test_runners(test_infos):
+    """Group the test_infos by test runners
+
+    Args:
+        test_infos: List of TestInfo.
+
+    Returns:
+        List of tuples (test runner, tests).
+    """
+    tests_by_test_runner = []
+    test_runner_dict = _get_test_runners()
+    key = lambda x: x.test_runner
+    sorted_test_infos = sorted(list(test_infos), key=key)
+    for test_runner, tests in itertools.groupby(sorted_test_infos, key):
+        # groupby returns a grouper object, we want to operate on a list.
+        tests = list(tests)
+        test_runner_class = test_runner_dict.get(test_runner)
+        if test_runner_class is None:
+            raise atest_error.UnknownTestRunnerError('Unknown Test Runner %s' %
+                                                     test_runner)
+        tests_by_test_runner.append((test_runner_class, tests))
+    return tests_by_test_runner
+
+
+def get_test_runner_reqs(module_info, test_infos):
+    """Returns the requirements for all test runners specified in the tests.
+
+    Args:
+        module_info: ModuleInfo object.
+        test_infos: List of TestInfo.
+
+    Returns:
+        Set of build targets required by the test runners.
+    """
+    dummy_result_dir = ''
+    test_runner_build_req = set()
+    for test_runner, _ in group_tests_by_test_runners(test_infos):
+        test_runner_build_req |= test_runner(
+            dummy_result_dir,
+            module_info=module_info).get_test_runner_build_reqs()
+    return test_runner_build_req
+
+
+def run_all_tests(results_dir, test_infos, extra_args,
+                  delay_print_summary=False):
+    """Run the given tests.
+
+    Args:
+        results_dir: String directory to store atest results.
+        test_infos: List of TestInfo.
+        extra_args: Dict of extra args for test runners to use.
+
+    Returns:
+        0 if tests succeed, non-zero otherwise.
+    """
+    reporter = result_reporter.ResultReporter()
+    reporter.print_starting_text()
+    tests_ret_code = constants.EXIT_CODE_SUCCESS
+    for test_runner, tests in group_tests_by_test_runners(test_infos):
+        test_name = ' '.join([test.test_name for test in tests])
+        test_start = time.time()
+        is_success = True
+        ret_code = constants.EXIT_CODE_TEST_FAILURE
+        stacktrace = ''
+        try:
+            test_runner = test_runner(results_dir)
+            ret_code = test_runner.run_tests(tests, extra_args, reporter)
+            tests_ret_code |= ret_code
+        # pylint: disable=broad-except
+        except Exception:
+            stacktrace = traceback.format_exc()
+            reporter.runner_failure(test_runner.NAME, stacktrace)
+            tests_ret_code = constants.EXIT_CODE_TEST_FAILURE
+            is_success = False
+        metrics.RunnerFinishEvent(
+            duration=metrics_utils.convert_duration(time.time() - test_start),
+            success=is_success,
+            runner_name=test_runner.NAME,
+            test=[{'name': test_name,
+                   'result': ret_code,
+                   'stacktrace': stacktrace}])
+    if delay_print_summary:
+        return tests_ret_code, reporter
+    return reporter.print_summary() or tests_ret_code, reporter
diff --git a/atest/test_runner_handler_unittest.py b/atest/test_runner_handler_unittest.py
new file mode 100755
index 0000000..b5a430e
--- /dev/null
+++ b/atest/test_runner_handler_unittest.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+#
+# Copyright 2017, 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 test_runner_handler."""
+
+# pylint: disable=protected-access
+
+import unittest
+import mock
+
+import atest_error
+import test_runner_handler
+from metrics import metrics
+from test_finders import test_info
+from test_runners import test_runner_base as tr_base
+
+FAKE_TR_NAME_A = 'FakeTestRunnerA'
+FAKE_TR_NAME_B = 'FakeTestRunnerB'
+MISSING_TR_NAME = 'MissingTestRunner'
+FAKE_TR_A_REQS = {'fake_tr_A_req1', 'fake_tr_A_req2'}
+FAKE_TR_B_REQS = {'fake_tr_B_req1', 'fake_tr_B_req2'}
+MODULE_NAME_A = 'ModuleNameA'
+MODULE_NAME_A_AGAIN = 'ModuleNameA_AGAIN'
+MODULE_NAME_B = 'ModuleNameB'
+MODULE_NAME_B_AGAIN = 'ModuleNameB_AGAIN'
+MODULE_INFO_A = test_info.TestInfo(MODULE_NAME_A, FAKE_TR_NAME_A, set())
+MODULE_INFO_A_AGAIN = test_info.TestInfo(MODULE_NAME_A_AGAIN, FAKE_TR_NAME_A,
+                                         set())
+MODULE_INFO_B = test_info.TestInfo(MODULE_NAME_B, FAKE_TR_NAME_B, set())
+MODULE_INFO_B_AGAIN = test_info.TestInfo(MODULE_NAME_B_AGAIN, FAKE_TR_NAME_B,
+                                         set())
+BAD_TESTINFO = test_info.TestInfo('bad_name', MISSING_TR_NAME, set())
+
+class FakeTestRunnerA(tr_base.TestRunnerBase):
+    """Fake test runner A."""
+
+    NAME = FAKE_TR_NAME_A
+    EXECUTABLE = 'echo'
+
+    def run_tests(self, test_infos, extra_args, reporter):
+        return 0
+
+    def host_env_check(self):
+        pass
+
+    def get_test_runner_build_reqs(self):
+        return FAKE_TR_A_REQS
+
+    def generate_run_commands(self, test_infos, extra_args, port=None):
+        return ['fake command']
+
+
+class FakeTestRunnerB(FakeTestRunnerA):
+    """Fake test runner B."""
+
+    NAME = FAKE_TR_NAME_B
+
+    def run_tests(self, test_infos, extra_args, reporter):
+        return 1
+
+    def get_test_runner_build_reqs(self):
+        return FAKE_TR_B_REQS
+
+
+class TestRunnerHandlerUnittests(unittest.TestCase):
+    """Unit tests for test_runner_handler.py"""
+
+    _TEST_RUNNERS = {
+        FakeTestRunnerA.NAME: FakeTestRunnerA,
+        FakeTestRunnerB.NAME: FakeTestRunnerB,
+    }
+
+    def setUp(self):
+        mock.patch('test_runner_handler._get_test_runners',
+                   return_value=self._TEST_RUNNERS).start()
+
+    def tearDown(self):
+        mock.patch.stopall()
+
+    def test_group_tests_by_test_runners(self):
+        """Test that we properly group tests by test runners."""
+        # Happy path testing.
+        test_infos = [MODULE_INFO_A, MODULE_INFO_A_AGAIN, MODULE_INFO_B,
+                      MODULE_INFO_B_AGAIN]
+        want_list = [(FakeTestRunnerA, [MODULE_INFO_A, MODULE_INFO_A_AGAIN]),
+                     (FakeTestRunnerB, [MODULE_INFO_B, MODULE_INFO_B_AGAIN])]
+        self.assertEqual(
+            want_list,
+            test_runner_handler.group_tests_by_test_runners(test_infos))
+
+        # Let's make sure we fail as expected.
+        self.assertRaises(
+            atest_error.UnknownTestRunnerError,
+            test_runner_handler.group_tests_by_test_runners, [BAD_TESTINFO])
+
+    def test_get_test_runner_reqs(self):
+        """Test that we get all the reqs from the test runners."""
+        test_infos = [MODULE_INFO_A, MODULE_INFO_B]
+        want_set = FAKE_TR_A_REQS | FAKE_TR_B_REQS
+        empty_module_info = None
+        self.assertEqual(
+            want_set,
+            test_runner_handler.get_test_runner_reqs(empty_module_info,
+                                                     test_infos))
+
+    @mock.patch.object(metrics, 'RunnerFinishEvent')
+    def test_run_all_tests(self, _mock_runner_finish):
+        """Test that the return value as we expected."""
+        results_dir = ""
+        extra_args = []
+        # Tests both run_tests return 0
+        test_infos = [MODULE_INFO_A, MODULE_INFO_A_AGAIN]
+        self.assertEqual(
+            0,
+            test_runner_handler.run_all_tests(
+                results_dir, test_infos, extra_args)[0])
+        # Tests both run_tests return 1
+        test_infos = [MODULE_INFO_B, MODULE_INFO_B_AGAIN]
+        self.assertEqual(
+            1,
+            test_runner_handler.run_all_tests(
+                results_dir, test_infos, extra_args)[0])
+        # Tests with on run_tests return 0, the other return 1
+        test_infos = [MODULE_INFO_A, MODULE_INFO_B]
+        self.assertEqual(
+            1,
+            test_runner_handler.run_all_tests(
+                results_dir, test_infos, extra_args)[0])
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_runners/__init__.py b/atest/test_runners/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/test_runners/__init__.py
diff --git a/atest/test_runners/atest_tf_test_runner.py b/atest/test_runners/atest_tf_test_runner.py
new file mode 100644
index 0000000..976a0ff
--- /dev/null
+++ b/atest/test_runners/atest_tf_test_runner.py
@@ -0,0 +1,625 @@
+# Copyright 2017, 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 Tradefed test runner class.
+"""
+
+from __future__ import print_function
+import json
+import logging
+import os
+import re
+import select
+import socket
+import subprocess
+
+from functools import partial
+
+# pylint: disable=import-error
+import atest_utils
+import constants
+import result_reporter
+from event_handler import EventHandler
+from test_finders import test_info
+from test_runners import test_runner_base
+
+POLL_FREQ_SECS = 10
+SOCKET_HOST = '127.0.0.1'
+SOCKET_QUEUE_MAX = 1
+SOCKET_BUFFER = 4096
+SELECT_TIMEOUT = 5
+
+# Socket Events of form FIRST_EVENT {JSON_DATA}\nSECOND_EVENT {JSON_DATA}
+# EVENT_RE has groups for the name and the data. "." does not match \n.
+EVENT_RE = re.compile(r'^(?P<event_name>[A-Z_]+) (?P<json_data>{.*})(?:\n|$)')
+
+EXEC_DEPENDENCIES = ('adb', 'aapt')
+
+TRADEFED_EXIT_MSG = ('TradeFed subprocess exited early with exit code=%s.')
+
+LOG_FOLDER_NAME = 'log'
+
+_INTEGRATION_FINDERS = frozenset(['', 'INTEGRATION', 'INTEGRATION_FILE_PATH'])
+
+class TradeFedExitError(Exception):
+    """Raised when TradeFed exists before test run has finished."""
+
+
+class AtestTradefedTestRunner(test_runner_base.TestRunnerBase):
+    """TradeFed Test Runner class."""
+    NAME = 'AtestTradefedTestRunner'
+    EXECUTABLE = 'atest_tradefed.sh'
+    _TF_TEMPLATE = 'template/atest_local_min'
+    # Use --no-enable-granular-attempts to control reporter replay behavior.
+    # TODO(b/142630648): Enable option enable-granular-attempts in sharding mode.
+    _LOG_ARGS = ('--logcat-on-failure --atest-log-file-path={log_path} '
+                 '--no-enable-granular-attempts')
+    _RUN_CMD = ('{exe} {template} --template:map '
+                'test=atest {log_args} {args}')
+    _BUILD_REQ = {'tradefed-core'}
+    _RERUN_OPTION_GROUP = [constants.ITERATIONS,
+                           constants.RERUN_UNTIL_FAILURE,
+                           constants.RETRY_ANY_FAILURE]
+
+    def __init__(self, results_dir, module_info=None, **kwargs):
+        """Init stuff for base class."""
+        super(AtestTradefedTestRunner, self).__init__(results_dir, **kwargs)
+        self.module_info = module_info
+        self.log_path = os.path.join(results_dir, LOG_FOLDER_NAME)
+        if not os.path.exists(self.log_path):
+            os.makedirs(self.log_path)
+        log_args = {'log_path': self.log_path}
+        self.run_cmd_dict = {'exe': self.EXECUTABLE,
+                             'template': self._TF_TEMPLATE,
+                             'args': '',
+                             'log_args': self._LOG_ARGS.format(**log_args)}
+        self.is_verbose = logging.getLogger().isEnabledFor(logging.DEBUG)
+        self.root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
+
+    def _try_set_gts_authentication_key(self):
+        """Set GTS authentication key if it is available or exists.
+
+        Strategy:
+            Get APE_API_KEY from os.environ:
+                - If APE_API_KEY is already set by user -> do nothing.
+            Get the APE_API_KEY from constants:
+                - If the key file exists -> set to env var.
+            If APE_API_KEY isn't set and the key file doesn't exist:
+                - Warn user some GTS tests may fail without authentication.
+        """
+        if os.environ.get('APE_API_KEY'):
+            logging.debug('APE_API_KEY is set by developer.')
+            return
+        ape_api_key = constants.GTS_GOOGLE_SERVICE_ACCOUNT
+        key_path = os.path.join(self.root_dir, ape_api_key)
+        if ape_api_key and os.path.exists(key_path):
+            logging.debug('Set APE_API_KEY: %s', ape_api_key)
+            os.environ['APE_API_KEY'] = ape_api_key
+        else:
+            logging.debug('APE_API_KEY not set, some GTS tests may fail'
+                          ' without authentication.')
+
+    def run_tests(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos. See base class for more.
+
+        Args:
+            test_infos: A list of TestInfos.
+            extra_args: Dict of extra args to add to test run.
+            reporter: An instance of result_report.ResultReporter.
+
+        Returns:
+            0 if tests succeed, non-zero otherwise.
+        """
+        reporter.log_path = self.log_path
+        reporter.rerun_options = self._extract_rerun_options(extra_args)
+        # Set google service key if it's available or found before running tests.
+        self._try_set_gts_authentication_key()
+        if os.getenv(test_runner_base.OLD_OUTPUT_ENV_VAR):
+            return self.run_tests_raw(test_infos, extra_args, reporter)
+        return self.run_tests_pretty(test_infos, extra_args, reporter)
+
+    def run_tests_raw(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos. See base class for more.
+
+        Args:
+            test_infos: A list of TestInfos.
+            extra_args: Dict of extra args to add to test run.
+            reporter: An instance of result_report.ResultReporter.
+
+        Returns:
+            0 if tests succeed, non-zero otherwise.
+        """
+        iterations = self._generate_iterations(extra_args)
+        reporter.register_unsupported_runner(self.NAME)
+
+        ret_code = constants.EXIT_CODE_SUCCESS
+        for _ in range(iterations):
+            run_cmds = self.generate_run_commands(test_infos, extra_args)
+            subproc = self.run(run_cmds[0], output_to_stdout=True)
+            ret_code |= self.wait_for_subprocess(subproc)
+        return ret_code
+
+    def run_tests_pretty(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos. See base class for more.
+
+        Args:
+            test_infos: A list of TestInfos.
+            extra_args: Dict of extra args to add to test run.
+            reporter: An instance of result_report.ResultReporter.
+
+        Returns:
+            0 if tests succeed, non-zero otherwise.
+        """
+        iterations = self._generate_iterations(extra_args)
+        ret_code = constants.EXIT_CODE_SUCCESS
+        for _ in range(iterations):
+            server = self._start_socket_server()
+            run_cmds = self.generate_run_commands(test_infos, extra_args,
+                                                  server.getsockname()[1])
+            subproc = self.run(run_cmds[0], output_to_stdout=self.is_verbose)
+            self.handle_subprocess(subproc, partial(self._start_monitor,
+                                                    server,
+                                                    subproc,
+                                                    reporter))
+            server.close()
+            ret_code |= self.wait_for_subprocess(subproc)
+        return ret_code
+
+    # pylint: disable=too-many-branches
+    def _start_monitor(self, server, tf_subproc, reporter):
+        """Polling and process event.
+
+        Args:
+            server: Socket server object.
+            tf_subproc: The tradefed subprocess to poll.
+            reporter: Result_Reporter object.
+        """
+        inputs = [server]
+        event_handlers = {}
+        data_map = {}
+        inv_socket = None
+        while inputs:
+            try:
+                readable, _, _ = select.select(inputs, [], [], SELECT_TIMEOUT)
+                for socket_object in readable:
+                    if socket_object is server:
+                        conn, addr = socket_object.accept()
+                        logging.debug('Accepted connection from %s', addr)
+                        conn.setblocking(False)
+                        inputs.append(conn)
+                        data_map[conn] = ''
+                        # The First connection should be invocation level reporter.
+                        if not inv_socket:
+                            inv_socket = conn
+                    else:
+                        # Count invocation level reporter events
+                        # without showing real-time information.
+                        if inv_socket == socket_object:
+                            reporter.silent = True
+                            event_handler = event_handlers.setdefault(
+                                socket_object, EventHandler(reporter, self.NAME))
+                        else:
+                            event_handler = event_handlers.setdefault(
+                                socket_object, EventHandler(
+                                    result_reporter.ResultReporter(), self.NAME))
+                        recv_data = self._process_connection(data_map,
+                                                             socket_object,
+                                                             event_handler)
+                        if not recv_data:
+                            inputs.remove(socket_object)
+                            socket_object.close()
+            finally:
+                if tf_subproc.poll() is not None:
+                    while inputs:
+                        inputs.pop(0).close()
+                    if not data_map:
+                        raise TradeFedExitError(TRADEFED_EXIT_MSG
+                                                % tf_subproc.returncode)
+
+    def _process_connection(self, data_map, conn, event_handler):
+        """Process a socket connection betwen TF and ATest.
+
+        Expect data of form EVENT_NAME {JSON_DATA}.  Multiple events will be
+        \n deliminated.  Need to buffer data in case data exceeds socket
+        buffer.
+        E.q.
+            TEST_RUN_STARTED {runName":"hello_world_test","runAttempt":0}\n
+            TEST_STARTED {"start_time":2172917, "testName":"PrintHelloWorld"}\n
+        Args:
+            data_map: The data map of all connections.
+            conn: Socket connection.
+            event_handler: EventHandler object.
+
+        Returns:
+            True if conn.recv() has data , False otherwise.
+        """
+        # Set connection into blocking mode.
+        conn.settimeout(None)
+        data = conn.recv(SOCKET_BUFFER)
+        logging.debug('received: %s', data)
+        if data:
+            data_map[conn] += data
+            while True:
+                match = EVENT_RE.match(data_map[conn])
+                if not match:
+                    break
+                try:
+                    event_data = json.loads(match.group('json_data'))
+                except ValueError:
+                    logging.debug('Json incomplete, wait for more data')
+                    break
+                event_name = match.group('event_name')
+                event_handler.process_event(event_name, event_data)
+                data_map[conn] = data_map[conn][match.end():]
+        return bool(data)
+
+    def _start_socket_server(self):
+        """Start a TCP server."""
+        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        # Port 0 lets the OS pick an open port between 1024 and 65535.
+        server.bind((SOCKET_HOST, 0))
+        server.listen(SOCKET_QUEUE_MAX)
+        server.settimeout(POLL_FREQ_SECS)
+        logging.debug('Socket server started on port %s',
+                      server.getsockname()[1])
+        return server
+
+    def host_env_check(self):
+        """Check that host env has everything we need.
+
+        We actually can assume the host env is fine because we have the same
+        requirements that atest has. Update this to check for android env vars
+        if that changes.
+        """
+        pass
+
+    @staticmethod
+    def _is_missing_exec(executable):
+        """Check if system build executable is available.
+
+        Args:
+            executable: Executable we are checking for.
+        Returns:
+            True if executable is missing, False otherwise.
+        """
+        try:
+            output = subprocess.check_output(['which', executable])
+        except subprocess.CalledProcessError:
+            return True
+        # TODO: Check if there is a clever way to determine if system adb is
+        # good enough.
+        root_dir = os.environ.get(constants.ANDROID_BUILD_TOP)
+        return os.path.commonprefix([output, root_dir]) != root_dir
+
+    def get_test_runner_build_reqs(self):
+        """Return the build requirements.
+
+        Returns:
+            Set of build targets.
+        """
+        build_req = self._BUILD_REQ
+        # Use different base build requirements if google-tf is around.
+        if self.module_info.is_module(constants.GTF_MODULE):
+            build_req = {constants.GTF_TARGET}
+        # Always add ATest's own TF target.
+        build_req.add(constants.ATEST_TF_MODULE)
+        # Add adb if we can't find it.
+        for executable in EXEC_DEPENDENCIES:
+            if self._is_missing_exec(executable):
+                build_req.add(executable)
+        return build_req
+
+    # pylint: disable=too-many-branches
+    # pylint: disable=too-many-statements
+    @staticmethod
+    def _parse_extra_args(extra_args):
+        """Convert the extra args into something tf can understand.
+
+        Args:
+            extra_args: Dict of args
+
+        Returns:
+            Tuple of args to append and args not supported.
+        """
+        args_to_append = []
+        args_not_supported = []
+        for arg in extra_args:
+            if constants.WAIT_FOR_DEBUGGER == arg:
+                args_to_append.append('--wait-for-debugger')
+                continue
+            if constants.DISABLE_INSTALL == arg:
+                args_to_append.append('--disable-target-preparers')
+                continue
+            if constants.SERIAL == arg:
+                args_to_append.append('--serial')
+                args_to_append.append(extra_args[arg])
+                continue
+            if constants.DISABLE_TEARDOWN == arg:
+                args_to_append.append('--disable-teardown')
+                continue
+            if constants.HOST == arg:
+                args_to_append.append('-n')
+                args_to_append.append('--prioritize-host-config')
+                args_to_append.append('--skip-host-arch-check')
+                continue
+            if constants.CUSTOM_ARGS == arg:
+                # We might need to sanitize it prior to appending but for now
+                # let's just treat it like a simple arg to pass on through.
+                args_to_append.extend(extra_args[arg])
+                continue
+            if constants.ALL_ABI == arg:
+                args_to_append.append('--all-abi')
+                continue
+            if constants.DRY_RUN == arg:
+                continue
+            if constants.INSTANT == arg:
+                args_to_append.append('--enable-parameterized-modules')
+                args_to_append.append('--module-parameter')
+                args_to_append.append('instant_app')
+                continue
+            if constants.USER_TYPE == arg:
+                args_to_append.append('--enable-parameterized-modules')
+                args_to_append.append('--enable-optional-parameterization')
+                args_to_append.append('--module-parameter')
+                args_to_append.append(extra_args[arg])
+            if constants.ITERATIONS == arg:
+                args_to_append.append('--retry-strategy')
+                args_to_append.append(constants.ITERATIONS)
+                args_to_append.append('--max-testcase-run-count')
+                args_to_append.append(str(extra_args[arg]))
+                continue
+            if constants.RERUN_UNTIL_FAILURE == arg:
+                args_to_append.append('--retry-strategy')
+                args_to_append.append(constants.RERUN_UNTIL_FAILURE)
+                args_to_append.append('--max-testcase-run-count')
+                args_to_append.append(str(extra_args[arg]))
+                continue
+            if constants.RETRY_ANY_FAILURE == arg:
+                args_to_append.append('--retry-strategy')
+                args_to_append.append(constants.RETRY_ANY_FAILURE)
+                args_to_append.append('--max-testcase-run-count')
+                args_to_append.append(str(extra_args[arg]))
+                continue
+            args_not_supported.append(arg)
+        return args_to_append, args_not_supported
+
+    def _generate_metrics_folder(self, extra_args):
+        """Generate metrics folder."""
+        metrics_folder = ''
+        if extra_args.get(constants.PRE_PATCH_ITERATIONS):
+            metrics_folder = os.path.join(self.results_dir, 'baseline-metrics')
+        elif extra_args.get(constants.POST_PATCH_ITERATIONS):
+            metrics_folder = os.path.join(self.results_dir, 'new-metrics')
+        return metrics_folder
+
+    def _generate_iterations(self, extra_args):
+        """Generate iterations."""
+        iterations = 1
+        if extra_args.get(constants.PRE_PATCH_ITERATIONS):
+            iterations = extra_args.pop(constants.PRE_PATCH_ITERATIONS)
+        elif extra_args.get(constants.POST_PATCH_ITERATIONS):
+            iterations = extra_args.pop(constants.POST_PATCH_ITERATIONS)
+        return iterations
+
+    def generate_run_commands(self, test_infos, extra_args, port=None):
+        """Generate a single run command from TestInfos.
+
+        Args:
+            test_infos: A set of TestInfo instances.
+            extra_args: A Dict of extra args to append.
+            port: Optional. An int of the port number to send events to. If
+                  None, then subprocess reporter in TF won't try to connect.
+
+        Returns:
+            A list that contains the string of atest tradefed run command.
+            Only one command is returned.
+        """
+        args = self._create_test_args(test_infos)
+        metrics_folder = self._generate_metrics_folder(extra_args)
+
+        # Create a copy of args as more args could be added to the list.
+        test_args = list(args)
+        if port:
+            test_args.extend(['--subprocess-report-port', str(port)])
+        if metrics_folder:
+            test_args.extend(['--metrics-folder', metrics_folder])
+            logging.info('Saved metrics in: %s', metrics_folder)
+        log_level = 'VERBOSE' if self.is_verbose else 'WARN'
+        test_args.extend(['--log-level', log_level])
+
+        args_to_add, args_not_supported = self._parse_extra_args(extra_args)
+
+        # TODO(b/122889707) Remove this after finding the root cause.
+        env_serial = os.environ.get(constants.ANDROID_SERIAL)
+        # Use the env variable ANDROID_SERIAL if it's set by user but only when
+        # the target tests are not deviceless tests.
+        if env_serial and '--serial' not in args_to_add and '-n' not in args_to_add:
+            args_to_add.append("--serial")
+            args_to_add.append(env_serial)
+
+        test_args.extend(args_to_add)
+        if args_not_supported:
+            logging.info('%s does not support the following args %s',
+                         self.EXECUTABLE, args_not_supported)
+
+        test_args.extend(atest_utils.get_result_server_args())
+        self.run_cmd_dict['args'] = ' '.join(test_args)
+        return [self._RUN_CMD.format(**self.run_cmd_dict)]
+
+    def _flatten_test_infos(self, test_infos):
+        """Sort and group test_infos by module_name and sort and group filters
+        by class name.
+
+            Example of three test_infos in a set:
+                Module1, {(classA, {})}
+                Module1, {(classB, {Method1})}
+                Module1, {(classB, {Method2}}
+            Becomes a set with one element:
+                Module1, {(ClassA, {}), (ClassB, {Method1, Method2})}
+            Where:
+                  Each line is a test_info namedtuple
+                  {} = Frozenset
+                  () = TestFilter namedtuple
+
+        Args:
+            test_infos: A set of TestInfo namedtuples.
+
+        Returns:
+            A set of TestInfos flattened.
+        """
+        results = set()
+        key = lambda x: x.test_name
+        for module, group in atest_utils.sort_and_group(test_infos, key):
+            # module is a string, group is a generator of grouped TestInfos.
+            # Module Test, so flatten test_infos:
+            no_filters = False
+            filters = set()
+            test_runner = None
+            test_finder = None
+            build_targets = set()
+            data = {}
+            module_args = []
+            for test_info_i in group:
+                data.update(test_info_i.data)
+                # Extend data with constants.TI_MODULE_ARG instead of overwriting.
+                module_args.extend(test_info_i.data.get(constants.TI_MODULE_ARG, []))
+                test_runner = test_info_i.test_runner
+                test_finder = test_info_i.test_finder
+                build_targets |= test_info_i.build_targets
+                test_filters = test_info_i.data.get(constants.TI_FILTER)
+                if not test_filters or no_filters:
+                    # test_info wants whole module run, so hardcode no filters.
+                    no_filters = True
+                    filters = set()
+                    continue
+                filters |= test_filters
+            if module_args:
+                data[constants.TI_MODULE_ARG] = module_args
+            data[constants.TI_FILTER] = self._flatten_test_filters(filters)
+            results.add(
+                test_info.TestInfo(test_name=module,
+                                   test_runner=test_runner,
+                                   test_finder=test_finder,
+                                   build_targets=build_targets,
+                                   data=data))
+        return results
+
+    @staticmethod
+    def _flatten_test_filters(filters):
+        """Sort and group test_filters by class_name.
+
+            Example of three test_filters in a frozenset:
+                classA, {}
+                classB, {Method1}
+                classB, {Method2}
+            Becomes a frozenset with these elements:
+                classA, {}
+                classB, {Method1, Method2}
+            Where:
+                Each line is a TestFilter namedtuple
+                {} = Frozenset
+
+        Args:
+            filters: A frozenset of test_filters.
+
+        Returns:
+            A frozenset of test_filters flattened.
+        """
+        results = set()
+        key = lambda x: x.class_name
+        for class_name, group in atest_utils.sort_and_group(filters, key):
+            # class_name is a string, group is a generator of TestFilters
+            assert class_name is not None
+            methods = set()
+            for test_filter in group:
+                if not test_filter.methods:
+                    # Whole class should be run
+                    methods = set()
+                    break
+                methods |= test_filter.methods
+            results.add(test_info.TestFilter(class_name, frozenset(methods)))
+        return frozenset(results)
+
+    def _create_test_args(self, test_infos):
+        """Compile TF command line args based on the given test infos.
+
+        Args:
+            test_infos: A set of TestInfo instances.
+
+        Returns: A list of TF arguments to run the tests.
+        """
+        args = []
+        if not test_infos:
+            return []
+
+        # Only need to check one TestInfo to determine if the tests are
+        # configured in TEST_MAPPING.
+        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()
+        has_integration_test = False
+        for info in test_infos:
+            # Integration test exists in TF's jar, so it must have the option
+            # if it's integration finder.
+            if info.test_finder in _INTEGRATION_FINDERS:
+                has_integration_test = True
+            args.extend([constants.TF_INCLUDE_FILTER, info.test_name])
+            filters = set()
+            for test_filter in info.data.get(constants.TI_FILTER, []):
+                filters.update(test_filter.to_set_of_tf_strings())
+            for test_filter in filters:
+                filter_arg = constants.TF_ATEST_INCLUDE_FILTER_VALUE_FMT.format(
+                    test_name=info.test_name, test_filter=test_filter)
+                args.extend([constants.TF_ATEST_INCLUDE_FILTER, filter_arg])
+            for option in info.data.get(constants.TI_MODULE_ARG, []):
+                if constants.TF_INCLUDE_FILTER_OPTION == option[0]:
+                    suite_filter = (
+                        constants.TF_SUITE_FILTER_ARG_VALUE_FMT.format(
+                            test_name=info.test_name, option_value=option[1]))
+                    args.extend([constants.TF_INCLUDE_FILTER, suite_filter])
+                elif constants.TF_EXCLUDE_FILTER_OPTION == option[0]:
+                    suite_filter = (
+                        constants.TF_SUITE_FILTER_ARG_VALUE_FMT.format(
+                            test_name=info.test_name, option_value=option[1]))
+                    args.extend([constants.TF_EXCLUDE_FILTER, suite_filter])
+                else:
+                    module_arg = (
+                        constants.TF_MODULE_ARG_VALUE_FMT.format(
+                            test_name=info.test_name, option_name=option[0],
+                            option_value=option[1]))
+                    args.extend([constants.TF_MODULE_ARG, module_arg])
+        # TODO (b/141090547) Pass the config path to TF to load configs.
+        # Compile option in TF if finder is not INTEGRATION or not set.
+        if not has_integration_test:
+            args.append(constants.TF_SKIP_LOADING_CONFIG_JAR)
+        return args
+
+    def _extract_rerun_options(self, extra_args):
+        """Extract rerun options to a string for output.
+
+        Args:
+            extra_args: Dict of extra args for test runners to use.
+
+        Returns: A string of rerun options.
+        """
+        extracted_options = ['{} {}'.format(arg, extra_args[arg])
+                             for arg in extra_args
+                             if arg in self._RERUN_OPTION_GROUP]
+        return ' '.join(extracted_options)
diff --git a/atest/test_runners/atest_tf_test_runner_unittest.py b/atest/test_runners/atest_tf_test_runner_unittest.py
new file mode 100755
index 0000000..022bd97
--- /dev/null
+++ b/atest/test_runners/atest_tf_test_runner_unittest.py
@@ -0,0 +1,531 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+
+"""Unittests for atest_tf_test_runner."""
+
+import os
+import sys
+import tempfile
+import unittest
+import json
+import mock
+
+# pylint: disable=import-error
+import constants
+import unittest_constants as uc
+import unittest_utils
+import atest_tf_test_runner as atf_tr
+import event_handler
+from test_finders import test_info
+
+if sys.version_info[0] == 2:
+    from StringIO import StringIO
+else:
+    from io import StringIO
+
+#pylint: disable=protected-access
+#pylint: disable=invalid-name
+TEST_INFO_DIR = '/tmp/atest_run_1510085893_pi_Nbi'
+METRICS_DIR = '%s/baseline-metrics' % TEST_INFO_DIR
+METRICS_DIR_ARG = '--metrics-folder %s ' % METRICS_DIR
+RUN_CMD_ARGS = '{metrics}--log-level WARN{serial}'
+LOG_ARGS = atf_tr.AtestTradefedTestRunner._LOG_ARGS.format(
+    log_path=os.path.join(TEST_INFO_DIR, atf_tr.LOG_FOLDER_NAME))
+RUN_CMD = atf_tr.AtestTradefedTestRunner._RUN_CMD.format(
+    exe=atf_tr.AtestTradefedTestRunner.EXECUTABLE,
+    template=atf_tr.AtestTradefedTestRunner._TF_TEMPLATE,
+    args=RUN_CMD_ARGS,
+    log_args=LOG_ARGS)
+FULL_CLASS2_NAME = 'android.jank.cts.ui.SomeOtherClass'
+CLASS2_FILTER = test_info.TestFilter(FULL_CLASS2_NAME, frozenset())
+METHOD2_FILTER = test_info.TestFilter(uc.FULL_CLASS_NAME, frozenset([uc.METHOD2_NAME]))
+MODULE_ARG1 = [(constants.TF_INCLUDE_FILTER_OPTION, "A"),
+               (constants.TF_INCLUDE_FILTER_OPTION, "B")]
+MODULE_ARG2 = []
+CLASS2_METHOD_FILTER = test_info.TestFilter(FULL_CLASS2_NAME,
+                                            frozenset([uc.METHOD_NAME, uc.METHOD2_NAME]))
+MODULE2_INFO = test_info.TestInfo(uc.MODULE2_NAME,
+                                  atf_tr.AtestTradefedTestRunner.NAME,
+                                  set(),
+                                  data={constants.TI_REL_CONFIG: uc.CONFIG2_FILE,
+                                        constants.TI_FILTER: frozenset()})
+CLASS1_BUILD_TARGETS = {'class_1_build_target'}
+CLASS1_INFO = test_info.TestInfo(uc.MODULE_NAME,
+                                 atf_tr.AtestTradefedTestRunner.NAME,
+                                 CLASS1_BUILD_TARGETS,
+                                 data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+                                       constants.TI_FILTER: frozenset([uc.CLASS_FILTER])})
+CLASS2_BUILD_TARGETS = {'class_2_build_target'}
+CLASS2_INFO = test_info.TestInfo(uc.MODULE_NAME,
+                                 atf_tr.AtestTradefedTestRunner.NAME,
+                                 CLASS2_BUILD_TARGETS,
+                                 data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+                                       constants.TI_FILTER: frozenset([CLASS2_FILTER])})
+CLASS3_BUILD_TARGETS = {'class_3_build_target'}
+CLASS3_INFO = test_info.TestInfo(uc.MODULE_NAME,
+                                 atf_tr.AtestTradefedTestRunner.NAME,
+                                 CLASS3_BUILD_TARGETS,
+                                 data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+                                       constants.TI_FILTER: frozenset(),
+                                       constants.TI_MODULE_ARG: MODULE_ARG1})
+CLASS4_BUILD_TARGETS = {'class_4_build_target'}
+CLASS4_INFO = test_info.TestInfo(uc.MODULE_NAME,
+                                 atf_tr.AtestTradefedTestRunner.NAME,
+                                 CLASS4_BUILD_TARGETS,
+                                 data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+                                       constants.TI_FILTER: frozenset(),
+                                       constants.TI_MODULE_ARG: MODULE_ARG2})
+CLASS1_CLASS2_MODULE_INFO = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    uc.MODULE_BUILD_TARGETS | CLASS1_BUILD_TARGETS | CLASS2_BUILD_TARGETS,
+    uc.MODULE_DATA)
+FLAT_CLASS_INFO = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    CLASS1_BUILD_TARGETS | CLASS2_BUILD_TARGETS,
+    data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+          constants.TI_FILTER: frozenset([uc.CLASS_FILTER, CLASS2_FILTER])})
+FLAT2_CLASS_INFO = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    CLASS3_BUILD_TARGETS | CLASS4_BUILD_TARGETS,
+    data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+          constants.TI_FILTER: frozenset(),
+          constants.TI_MODULE_ARG: MODULE_ARG1 + MODULE_ARG2})
+GTF_INT_CONFIG = os.path.join(uc.GTF_INT_DIR, uc.GTF_INT_NAME + '.xml')
+CLASS2_METHOD_INFO = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    set(),
+    data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+          constants.TI_FILTER:
+              frozenset([test_info.TestFilter(
+                  FULL_CLASS2_NAME, frozenset([uc.METHOD_NAME, uc.METHOD2_NAME]))])})
+METHOD_AND_CLASS2_METHOD = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    uc.MODULE_BUILD_TARGETS,
+    data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+          constants.TI_FILTER: frozenset([uc.METHOD_FILTER, CLASS2_METHOD_FILTER])})
+METHOD_METHOD2_AND_CLASS2_METHOD = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    uc.MODULE_BUILD_TARGETS,
+    data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+          constants.TI_FILTER: frozenset([uc.FLAT_METHOD_FILTER, CLASS2_METHOD_FILTER])})
+METHOD2_INFO = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    set(),
+    data={constants.TI_REL_CONFIG: uc.CONFIG_FILE,
+          constants.TI_FILTER: frozenset([METHOD2_FILTER])})
+
+INT_INFO = test_info.TestInfo(
+    uc.INT_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    set(),
+    test_finder='INTEGRATION')
+
+MOD_INFO = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    set(),
+    test_finder='MODULE')
+
+MOD_INFO_NO_TEST_FINDER = test_info.TestInfo(
+    uc.MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    set())
+
+EVENTS_NORMAL = [
+    ('TEST_MODULE_STARTED', {
+        'moduleContextFileName':'serial-util1146216{974}2772610436.ser',
+        'moduleName':'someTestModule'}),
+    ('TEST_RUN_STARTED', {'testCount': 2}),
+    ('TEST_STARTED', {'start_time':52, 'className':'someClassName',
+                      'testName':'someTestName'}),
+    ('TEST_ENDED', {'end_time':1048, 'className':'someClassName',
+                    'testName':'someTestName'}),
+    ('TEST_STARTED', {'start_time':48, 'className':'someClassName2',
+                      'testName':'someTestName2'}),
+    ('TEST_FAILED', {'className':'someClassName2', 'testName':'someTestName2',
+                     'trace': 'someTrace'}),
+    ('TEST_ENDED', {'end_time':9876450, 'className':'someClassName2',
+                    'testName':'someTestName2'}),
+    ('TEST_RUN_ENDED', {}),
+    ('TEST_MODULE_ENDED', {'foo': 'bar'}),
+]
+
+class AtestTradefedTestRunnerUnittests(unittest.TestCase):
+    """Unit tests for atest_tf_test_runner.py"""
+
+    def setUp(self):
+        self.tr = atf_tr.AtestTradefedTestRunner(results_dir=TEST_INFO_DIR)
+
+    def tearDown(self):
+        mock.patch.stopall()
+
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner,
+                       '_start_socket_server')
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner,
+                       'run')
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner,
+                       '_create_test_args', return_value=['some_args'])
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner,
+                       'generate_run_commands', return_value='some_cmd')
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner,
+                       '_process_connection', return_value=None)
+    @mock.patch('select.select')
+    @mock.patch('os.killpg', return_value=None)
+    @mock.patch('os.getpgid', return_value=None)
+    @mock.patch('signal.signal', return_value=None)
+    def test_run_tests_pretty(self, _signal, _pgid, _killpg, mock_select,
+                              _process, _run_cmd, _test_args,
+                              mock_run, mock_start_socket_server):
+        """Test _run_tests_pretty method."""
+        mock_subproc = mock.Mock()
+        mock_run.return_value = mock_subproc
+        mock_subproc.returncode = 0
+        mock_subproc.poll.side_effect = [True, None]
+        mock_server = mock.Mock()
+        mock_server.getsockname.return_value = ('', '')
+        mock_start_socket_server.return_value = mock_server
+        mock_reporter = mock.Mock()
+
+        # Test no early TF exit
+        mock_conn = mock.Mock()
+        mock_server.accept.return_value = (mock_conn, 'some_addr')
+        mock_server.close.return_value = True
+        mock_select.side_effect = [([mock_server], None, None),
+                                   ([mock_conn], None, None)]
+        self.tr.run_tests_pretty([MODULE2_INFO], {}, mock_reporter)
+
+        # Test early TF exit
+        tmp_file = tempfile.NamedTemporaryFile()
+        with open(tmp_file.name, 'w') as f:
+            f.write("tf msg")
+        self.tr.test_log_file = tmp_file
+        mock_select.side_effect = [([], None, None)]
+        mock_subproc.poll.side_effect = None
+        capture_output = StringIO()
+        sys.stdout = capture_output
+        self.assertRaises(atf_tr.TradeFedExitError, self.tr.run_tests_pretty,
+                          [MODULE2_INFO], {}, mock_reporter)
+        sys.stdout = sys.__stdout__
+        self.assertTrue('tf msg' in capture_output.getvalue())
+
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner, '_process_connection')
+    @mock.patch('select.select')
+    def test_start_monitor_2_connection(self, mock_select, mock_process):
+        """Test _start_monitor method."""
+        mock_server = mock.Mock()
+        mock_subproc = mock.Mock()
+        mock_reporter = mock.Mock()
+        mock_conn1 = mock.Mock()
+        mock_conn2 = mock.Mock()
+        mock_server.accept.side_effect = [(mock_conn1, 'addr 1'),
+                                          (mock_conn2, 'addr 2')]
+        mock_select.side_effect = [([mock_server], None, None),
+                                   ([mock_conn1], None, None),
+                                   ([mock_conn2], None, None)]
+        mock_subproc.poll.side_effect = [None, None, True]
+        self.tr._start_monitor(mock_server, mock_subproc, mock_reporter)
+        self.assertEqual(mock_process.call_count, 2)
+        calls = [mock.call.accept(), mock.call.close()]
+        mock_server.assert_has_calls(calls)
+        mock_conn1.assert_has_calls([mock.call.close()])
+        mock_conn1.assert_has_calls([mock.call.close()])
+
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner, '_process_connection')
+    @mock.patch('select.select')
+    def test_start_monitor_tf_exit_before_2nd_connection(self,
+                                                         mock_select,
+                                                         mock_process):
+        """Test _start_monitor method."""
+        mock_server = mock.Mock()
+        mock_subproc = mock.Mock()
+        mock_reporter = mock.Mock()
+        mock_conn1 = mock.Mock()
+        mock_conn2 = mock.Mock()
+        mock_server.accept.side_effect = [(mock_conn1, 'addr 1'),
+                                          (mock_conn2, 'addr 2')]
+        mock_select.side_effect = [([mock_server], None, None),
+                                   ([mock_conn1], None, None),
+                                   ([mock_conn2], None, None)]
+        mock_subproc.poll.side_effect = [None, True]
+        self.tr._start_monitor(mock_server, mock_subproc, mock_reporter)
+        self.assertEqual(mock_process.call_count, 1)
+        calls = [mock.call.accept(), mock.call.close()]
+        mock_server.assert_has_calls(calls)
+        mock_conn1.assert_has_calls([mock.call.close()])
+        mock_conn1.assert_has_calls([mock.call.close()])
+
+
+    def test_start_socket_server(self):
+        """Test start_socket_server method."""
+        server = self.tr._start_socket_server()
+        host, port = server.getsockname()
+        self.assertEquals(host, atf_tr.SOCKET_HOST)
+        self.assertLessEqual(port, 65535)
+        self.assertGreaterEqual(port, 1024)
+        server.close()
+
+    @mock.patch('os.path.exists')
+    @mock.patch.dict('os.environ', {'APE_API_KEY':'/tmp/123.json'})
+    def test_try_set_gts_authentication_key_is_set_by_user(self, mock_exist):
+        """Test try_set_authentication_key_is_set_by_user method."""
+        # Test key is set by user.
+        self.tr._try_set_gts_authentication_key()
+        mock_exist.assert_not_called()
+
+    @mock.patch('constants.GTS_GOOGLE_SERVICE_ACCOUNT')
+    @mock.patch('os.path.exists')
+    def test_try_set_gts_authentication_key_not_set(self, mock_exist, mock_key):
+        """Test try_set_authentication_key_not_set method."""
+        # Test key neither exists nor set by user.
+        mock_exist.return_value = False
+        mock_key.return_value = ''
+        self.tr._try_set_gts_authentication_key()
+        self.assertEquals(os.environ.get('APE_API_KEY'), None)
+
+    @mock.patch.object(event_handler.EventHandler, 'process_event')
+    def test_process_connection(self, mock_pe):
+        """Test _process_connection method."""
+        mock_socket = mock.Mock()
+        for name, data in EVENTS_NORMAL:
+            datas = {mock_socket: ''}
+            socket_data = '%s %s' % (name, json.dumps(data))
+            mock_socket.recv.return_value = socket_data
+            self.tr._process_connection(datas, mock_socket, mock_pe)
+
+        calls = [mock.call.process_event(name, data) for name, data in EVENTS_NORMAL]
+        mock_pe.assert_has_calls(calls)
+        mock_socket.recv.return_value = ''
+        self.assertFalse(self.tr._process_connection(datas, mock_socket, mock_pe))
+
+    @mock.patch.object(event_handler.EventHandler, 'process_event')
+    def test_process_connection_multiple_lines_in_single_recv(self, mock_pe):
+        """Test _process_connection when recv reads multiple lines in one go."""
+        mock_socket = mock.Mock()
+        squashed_events = '\n'.join(['%s %s' % (name, json.dumps(data))
+                                     for name, data in EVENTS_NORMAL])
+        socket_data = [squashed_events, '']
+        mock_socket.recv.side_effect = socket_data
+        datas = {mock_socket: ''}
+        self.tr._process_connection(datas, mock_socket, mock_pe)
+        calls = [mock.call.process_event(name, data) for name, data in EVENTS_NORMAL]
+        mock_pe.assert_has_calls(calls)
+
+    @mock.patch.object(event_handler.EventHandler, 'process_event')
+    def test_process_connection_with_buffering(self, mock_pe):
+        """Test _process_connection when events overflow socket buffer size"""
+        mock_socket = mock.Mock()
+        module_events = [EVENTS_NORMAL[0], EVENTS_NORMAL[-1]]
+        socket_events = ['%s %s' % (name, json.dumps(data))
+                         for name, data in module_events]
+        # test try-block code by breaking apart first event after first }
+        index = socket_events[0].index('}') + 1
+        socket_data = [socket_events[0][:index], socket_events[0][index:]]
+        # test non-try block buffering with second event
+        socket_data.extend([socket_events[1][:-4], socket_events[1][-4:], ''])
+        mock_socket.recv.side_effect = socket_data
+        datas = {mock_socket: ''}
+        self.tr._process_connection(datas, mock_socket, mock_pe)
+        self.tr._process_connection(datas, mock_socket, mock_pe)
+        self.tr._process_connection(datas, mock_socket, mock_pe)
+        self.tr._process_connection(datas, mock_socket, mock_pe)
+        calls = [mock.call.process_event(name, data) for name, data in module_events]
+        mock_pe.assert_has_calls(calls)
+
+    @mock.patch('os.environ.get', return_value=None)
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner, '_generate_metrics_folder')
+    @mock.patch('atest_utils.get_result_server_args')
+    def test_generate_run_commands_without_serial_env(self, mock_resultargs, mock_mertrics, _):
+        """Test generate_run_command method."""
+        # Basic Run Cmd
+        mock_resultargs.return_value = []
+        mock_mertrics.return_value = ''
+        unittest_utils.assert_strict_equal(
+            self,
+            self.tr.generate_run_commands([], {}),
+            [RUN_CMD.format(metrics='', serial='')])
+        mock_mertrics.return_value = METRICS_DIR
+        unittest_utils.assert_strict_equal(
+            self,
+            self.tr.generate_run_commands([], {}),
+            [RUN_CMD.format(metrics=METRICS_DIR_ARG, serial='')])
+        # Run cmd with result server args.
+        result_arg = '--result_arg'
+        mock_resultargs.return_value = [result_arg]
+        mock_mertrics.return_value = ''
+        unittest_utils.assert_strict_equal(
+            self,
+            self.tr.generate_run_commands([], {}),
+            [RUN_CMD.format(metrics='', serial='') + ' ' + result_arg])
+
+    @mock.patch('os.environ.get')
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner, '_generate_metrics_folder')
+    @mock.patch('atest_utils.get_result_server_args')
+    def test_generate_run_commands_with_serial_env(self, mock_resultargs, mock_mertrics, mock_env):
+        """Test generate_run_command method."""
+        # Basic Run Cmd
+        env_device_serial = 'env-device-0'
+        mock_resultargs.return_value = []
+        mock_mertrics.return_value = ''
+        mock_env.return_value = env_device_serial
+        env_serial_arg = ' --serial %s' % env_device_serial
+        # Serial env be set and without --serial arg.
+        unittest_utils.assert_strict_equal(
+            self,
+            self.tr.generate_run_commands([], {}),
+            [RUN_CMD.format(metrics='', serial=env_serial_arg)])
+        # Serial env be set but with --serial arg.
+        arg_device_serial = 'arg-device-0'
+        arg_serial_arg = ' --serial %s' % arg_device_serial
+        unittest_utils.assert_strict_equal(
+            self,
+            self.tr.generate_run_commands([], {constants.SERIAL:arg_device_serial}),
+            [RUN_CMD.format(metrics='', serial=arg_serial_arg)])
+        # Serial env be set but with -n arg
+        unittest_utils.assert_strict_equal(
+            self,
+            self.tr.generate_run_commands([], {constants.HOST}),
+            [RUN_CMD.format(metrics='', serial='') +
+             ' -n --prioritize-host-config --skip-host-arch-check'])
+
+
+    def test_flatten_test_filters(self):
+        """Test _flatten_test_filters method."""
+        # No Flattening
+        filters = self.tr._flatten_test_filters({uc.CLASS_FILTER})
+        unittest_utils.assert_strict_equal(self, frozenset([uc.CLASS_FILTER]),
+                                           filters)
+        filters = self.tr._flatten_test_filters({CLASS2_FILTER})
+        unittest_utils.assert_strict_equal(
+            self, frozenset([CLASS2_FILTER]), filters)
+        filters = self.tr._flatten_test_filters({uc.METHOD_FILTER})
+        unittest_utils.assert_strict_equal(
+            self, frozenset([uc.METHOD_FILTER]), filters)
+        filters = self.tr._flatten_test_filters({uc.METHOD_FILTER,
+                                                 CLASS2_METHOD_FILTER})
+        unittest_utils.assert_strict_equal(
+            self, frozenset([uc.METHOD_FILTER, CLASS2_METHOD_FILTER]), filters)
+        # Flattening
+        filters = self.tr._flatten_test_filters({uc.METHOD_FILTER,
+                                                 METHOD2_FILTER})
+        unittest_utils.assert_strict_equal(
+            self, filters, frozenset([uc.FLAT_METHOD_FILTER]))
+        filters = self.tr._flatten_test_filters({uc.METHOD_FILTER,
+                                                 METHOD2_FILTER,
+                                                 CLASS2_METHOD_FILTER,})
+        unittest_utils.assert_strict_equal(
+            self, filters, frozenset([uc.FLAT_METHOD_FILTER,
+                                      CLASS2_METHOD_FILTER]))
+
+    def test_flatten_test_infos(self):
+        """Test _flatten_test_infos method."""
+        # No Flattening
+        test_infos = self.tr._flatten_test_infos({uc.MODULE_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {uc.MODULE_INFO})
+
+        test_infos = self.tr._flatten_test_infos([uc.MODULE_INFO, MODULE2_INFO])
+        unittest_utils.assert_equal_testinfo_sets(
+            self, test_infos, {uc.MODULE_INFO, MODULE2_INFO})
+
+        test_infos = self.tr._flatten_test_infos({CLASS1_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {CLASS1_INFO})
+
+        test_infos = self.tr._flatten_test_infos({uc.INT_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {uc.INT_INFO})
+
+        test_infos = self.tr._flatten_test_infos({uc.METHOD_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {uc.METHOD_INFO})
+
+        # Flattening
+        test_infos = self.tr._flatten_test_infos({CLASS1_INFO, CLASS2_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {FLAT_CLASS_INFO})
+
+        test_infos = self.tr._flatten_test_infos({CLASS1_INFO, uc.INT_INFO,
+                                                  CLASS2_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {uc.INT_INFO,
+                                                   FLAT_CLASS_INFO})
+
+        test_infos = self.tr._flatten_test_infos({CLASS1_INFO, uc.MODULE_INFO,
+                                                  CLASS2_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {CLASS1_CLASS2_MODULE_INFO})
+
+        test_infos = self.tr._flatten_test_infos({MODULE2_INFO, uc.INT_INFO,
+                                                  CLASS1_INFO, CLASS2_INFO,
+                                                  uc.GTF_INT_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {uc.INT_INFO, uc.GTF_INT_INFO,
+                                                   FLAT_CLASS_INFO,
+                                                   MODULE2_INFO})
+
+        test_infos = self.tr._flatten_test_infos({uc.METHOD_INFO,
+                                                  CLASS2_METHOD_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {METHOD_AND_CLASS2_METHOD})
+
+        test_infos = self.tr._flatten_test_infos({uc.METHOD_INFO, METHOD2_INFO,
+                                                  CLASS2_METHOD_INFO})
+        unittest_utils.assert_equal_testinfo_sets(
+            self, test_infos, {METHOD_METHOD2_AND_CLASS2_METHOD})
+        test_infos = self.tr._flatten_test_infos({uc.METHOD_INFO, METHOD2_INFO,
+                                                  CLASS2_METHOD_INFO,
+                                                  MODULE2_INFO,
+                                                  uc.INT_INFO})
+        unittest_utils.assert_equal_testinfo_sets(
+            self, test_infos, {uc.INT_INFO, MODULE2_INFO,
+                               METHOD_METHOD2_AND_CLASS2_METHOD})
+
+        test_infos = self.tr._flatten_test_infos({CLASS3_INFO, CLASS4_INFO})
+        unittest_utils.assert_equal_testinfo_sets(self, test_infos,
+                                                  {FLAT2_CLASS_INFO})
+
+    def test_create_test_args(self):
+        """Test _create_test_args method."""
+        # Only compile '--skip-loading-config-jar' in TF if it's not
+        # INTEGRATION finder or the finder property isn't set.
+        args = self.tr._create_test_args([MOD_INFO])
+        self.assertTrue(constants.TF_SKIP_LOADING_CONFIG_JAR in args)
+
+        args = self.tr._create_test_args([INT_INFO])
+        self.assertFalse(constants.TF_SKIP_LOADING_CONFIG_JAR in args)
+
+        args = self.tr._create_test_args([MOD_INFO_NO_TEST_FINDER])
+        self.assertFalse(constants.TF_SKIP_LOADING_CONFIG_JAR in args)
+
+        args = self.tr._create_test_args([MOD_INFO_NO_TEST_FINDER, INT_INFO])
+        self.assertFalse(constants.TF_SKIP_LOADING_CONFIG_JAR in args)
+
+        args = self.tr._create_test_args([MOD_INFO_NO_TEST_FINDER])
+        self.assertFalse(constants.TF_SKIP_LOADING_CONFIG_JAR in args)
+
+        args = self.tr._create_test_args([MOD_INFO_NO_TEST_FINDER, INT_INFO, MOD_INFO])
+        self.assertFalse(constants.TF_SKIP_LOADING_CONFIG_JAR in args)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_runners/event_handler.py b/atest/test_runners/event_handler.py
new file mode 100644
index 0000000..efe0236
--- /dev/null
+++ b/atest/test_runners/event_handler.py
@@ -0,0 +1,287 @@
+# 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 test event handler class.
+"""
+
+from __future__ import print_function
+from collections import deque
+from datetime import timedelta
+import time
+import logging
+
+import atest_execution_info
+
+from test_runners import test_runner_base
+
+
+EVENT_NAMES = {'module_started': 'TEST_MODULE_STARTED',
+               'module_ended': 'TEST_MODULE_ENDED',
+               'run_started': 'TEST_RUN_STARTED',
+               'run_ended': 'TEST_RUN_ENDED',
+               # Next three are test-level events
+               'test_started': 'TEST_STARTED',
+               'test_failed': 'TEST_FAILED',
+               'test_ended': 'TEST_ENDED',
+               # Last two failures are runner-level, not test-level.
+               # Invocation failure is broader than run failure.
+               'run_failed': 'TEST_RUN_FAILED',
+               'invocation_failed': 'INVOCATION_FAILED',
+               'test_ignored': 'TEST_IGNORED',
+               'test_assumption_failure': 'TEST_ASSUMPTION_FAILURE',
+               'log_association': 'LOG_ASSOCIATION'}
+
+EVENT_PAIRS = {EVENT_NAMES['module_started']: EVENT_NAMES['module_ended'],
+               EVENT_NAMES['run_started']: EVENT_NAMES['run_ended'],
+               EVENT_NAMES['test_started']: EVENT_NAMES['test_ended']}
+START_EVENTS = list(EVENT_PAIRS.keys())
+END_EVENTS = list(EVENT_PAIRS.values())
+TEST_NAME_TEMPLATE = '%s#%s'
+EVENTS_NOT_BALANCED = ('Error: Saw %s Start event and %s End event. These '
+                       'should be equal!')
+
+# time in millisecond.
+ONE_SECOND = 1000
+ONE_MINUTE = 60000
+ONE_HOUR = 3600000
+
+CONNECTION_STATE = {
+    'current_test': None,
+    'test_run_name': None,
+    'last_failed': None,
+    'last_ignored': None,
+    'last_assumption_failed': None,
+    'current_group': None,
+    'current_group_total': None,
+    'test_count': 0,
+    'test_start_time': None}
+
+class EventHandleError(Exception):
+    """Raised when handle event error."""
+
+class EventHandler(object):
+    """Test Event handle class."""
+
+    def __init__(self, reporter, name):
+        self.reporter = reporter
+        self.runner_name = name
+        self.state = CONNECTION_STATE.copy()
+        self.event_stack = deque()
+
+    def _module_started(self, event_data):
+        if atest_execution_info.PREPARE_END_TIME is None:
+            atest_execution_info.PREPARE_END_TIME = time.time()
+        self.state['current_group'] = event_data['moduleName']
+        self.state['last_failed'] = None
+        self.state['current_test'] = None
+
+    def _run_started(self, event_data):
+        # Technically there can be more than one run per module.
+        self.state['test_run_name'] = event_data.setdefault('runName', '')
+        self.state['current_group_total'] = event_data['testCount']
+        self.state['test_count'] = 0
+        self.state['last_failed'] = None
+        self.state['current_test'] = None
+
+    def _test_started(self, event_data):
+        name = TEST_NAME_TEMPLATE % (event_data['className'],
+                                     event_data['testName'])
+        self.state['current_test'] = name
+        self.state['test_count'] += 1
+        self.state['test_start_time'] = event_data['start_time']
+
+    def _test_failed(self, event_data):
+        self.state['last_failed'] = {'name': TEST_NAME_TEMPLATE % (
+            event_data['className'],
+            event_data['testName']),
+                                     'trace': event_data['trace']}
+
+    def _test_ignored(self, event_data):
+        name = TEST_NAME_TEMPLATE % (event_data['className'],
+                                     event_data['testName'])
+        self.state['last_ignored'] = name
+
+    def _test_assumption_failure(self, event_data):
+        name = TEST_NAME_TEMPLATE % (event_data['className'],
+                                     event_data['testName'])
+        self.state['last_assumption_failed'] = name
+
+    def _run_failed(self, event_data):
+        # Module and Test Run probably started, but failure occurred.
+        self.reporter.process_test_result(test_runner_base.TestResult(
+            runner_name=self.runner_name,
+            group_name=self.state['current_group'],
+            test_name=self.state['current_test'],
+            status=test_runner_base.ERROR_STATUS,
+            details=event_data['reason'],
+            test_count=self.state['test_count'],
+            test_time='',
+            runner_total=None,
+            group_total=self.state['current_group_total'],
+            additional_info={},
+            test_run_name=self.state['test_run_name']))
+
+    def _invocation_failed(self, event_data):
+        # Broadest possible failure. May not even start the module/test run.
+        self.reporter.process_test_result(test_runner_base.TestResult(
+            runner_name=self.runner_name,
+            group_name=self.state['current_group'],
+            test_name=self.state['current_test'],
+            status=test_runner_base.ERROR_STATUS,
+            details=event_data['cause'],
+            test_count=self.state['test_count'],
+            test_time='',
+            runner_total=None,
+            group_total=self.state['current_group_total'],
+            additional_info={},
+            test_run_name=self.state['test_run_name']))
+
+    def _run_ended(self, event_data):
+        pass
+
+    def _module_ended(self, event_data):
+        pass
+
+    def _test_ended(self, event_data):
+        name = TEST_NAME_TEMPLATE % (event_data['className'],
+                                     event_data['testName'])
+        test_time = ''
+        if self.state['test_start_time']:
+            test_time = self._calc_duration(event_data['end_time'] -
+                                            self.state['test_start_time'])
+        if self.state['last_failed'] and name == self.state['last_failed']['name']:
+            status = test_runner_base.FAILED_STATUS
+            trace = self.state['last_failed']['trace']
+            self.state['last_failed'] = None
+        elif (self.state['last_assumption_failed'] and
+              name == self.state['last_assumption_failed']):
+            status = test_runner_base.ASSUMPTION_FAILED
+            self.state['last_assumption_failed'] = None
+            trace = None
+        elif self.state['last_ignored'] and name == self.state['last_ignored']:
+            status = test_runner_base.IGNORED_STATUS
+            self.state['last_ignored'] = None
+            trace = None
+        else:
+            status = test_runner_base.PASSED_STATUS
+            trace = None
+
+        default_event_keys = ['className', 'end_time', 'testName']
+        additional_info = {}
+        for event_key in event_data.keys():
+            if event_key not in default_event_keys:
+                additional_info[event_key] = event_data.get(event_key, None)
+
+        self.reporter.process_test_result(test_runner_base.TestResult(
+            runner_name=self.runner_name,
+            group_name=self.state['current_group'],
+            test_name=name,
+            status=status,
+            details=trace,
+            test_count=self.state['test_count'],
+            test_time=test_time,
+            runner_total=None,
+            additional_info=additional_info,
+            group_total=self.state['current_group_total'],
+            test_run_name=self.state['test_run_name']))
+
+    def _log_association(self, event_data):
+        pass
+
+    switch_handler = {EVENT_NAMES['module_started']: _module_started,
+                      EVENT_NAMES['run_started']: _run_started,
+                      EVENT_NAMES['test_started']: _test_started,
+                      EVENT_NAMES['test_failed']: _test_failed,
+                      EVENT_NAMES['test_ignored']: _test_ignored,
+                      EVENT_NAMES['test_assumption_failure']: _test_assumption_failure,
+                      EVENT_NAMES['run_failed']: _run_failed,
+                      EVENT_NAMES['invocation_failed']: _invocation_failed,
+                      EVENT_NAMES['test_ended']: _test_ended,
+                      EVENT_NAMES['run_ended']: _run_ended,
+                      EVENT_NAMES['module_ended']: _module_ended,
+                      EVENT_NAMES['log_association']: _log_association}
+
+    def process_event(self, event_name, event_data):
+        """Process the events of the test run and call reporter with results.
+
+        Args:
+            event_name: A string of the event name.
+            event_data: A dict of event data.
+        """
+        logging.debug('Processing %s %s', event_name, event_data)
+        if event_name in START_EVENTS:
+            self.event_stack.append(event_name)
+        elif event_name in END_EVENTS:
+            self._check_events_are_balanced(event_name, self.reporter)
+        if self.switch_handler.has_key(event_name):
+            self.switch_handler[event_name](self, event_data)
+        else:
+            # TODO(b/128875503): Implement the mechanism to inform not handled TF event.
+            logging.debug('Event[%s] is not processable.', event_name)
+
+    def _check_events_are_balanced(self, event_name, reporter):
+        """Check Start events and End events. They should be balanced.
+
+        If they are not balanced, print the error message in
+        state['last_failed'], then raise TradeFedExitError.
+
+        Args:
+            event_name: A string of the event name.
+            reporter: A ResultReporter instance.
+        Raises:
+            TradeFedExitError if we doesn't have a balance of START/END events.
+        """
+        start_event = self.event_stack.pop() if self.event_stack else None
+        if not start_event or EVENT_PAIRS[start_event] != event_name:
+            # Here bubble up the failed trace in the situation having
+            # TEST_FAILED but never receiving TEST_ENDED.
+            if self.state['last_failed'] and (start_event ==
+                                              EVENT_NAMES['test_started']):
+                reporter.process_test_result(test_runner_base.TestResult(
+                    runner_name=self.runner_name,
+                    group_name=self.state['current_group'],
+                    test_name=self.state['last_failed']['name'],
+                    status=test_runner_base.FAILED_STATUS,
+                    details=self.state['last_failed']['trace'],
+                    test_count=self.state['test_count'],
+                    test_time='',
+                    runner_total=None,
+                    group_total=self.state['current_group_total'],
+                    additional_info={},
+                    test_run_name=self.state['test_run_name']))
+            raise EventHandleError(EVENTS_NOT_BALANCED % (start_event,
+                                                          event_name))
+
+    @staticmethod
+    def _calc_duration(duration):
+        """Convert duration from ms to 3h2m43.034s.
+
+        Args:
+            duration: millisecond
+
+        Returns:
+            string in h:m:s, m:s, s or millis, depends on the duration.
+        """
+        delta = timedelta(milliseconds=duration)
+        timestamp = str(delta).split(':')  # hh:mm:microsec
+
+        if duration < ONE_SECOND:
+            return "({}ms)".format(duration)
+        elif duration < ONE_MINUTE:
+            return "({:.3f}s)".format(float(timestamp[2]))
+        elif duration < ONE_HOUR:
+            return "({0}m{1:.3f}s)".format(timestamp[1], float(timestamp[2]))
+        return "({0}h{1}m{2:.3f}s)".format(timestamp[0],
+                                           timestamp[1], float(timestamp[2]))
diff --git a/atest/test_runners/event_handler_unittest.py b/atest/test_runners/event_handler_unittest.py
new file mode 100755
index 0000000..09069b2
--- /dev/null
+++ b/atest/test_runners/event_handler_unittest.py
@@ -0,0 +1,348 @@
+#!/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 event_handler."""
+
+import unittest
+import mock
+
+import atest_tf_test_runner as atf_tr
+import event_handler as e_h
+from test_runners import test_runner_base
+
+
+EVENTS_NORMAL = [
+    ('TEST_MODULE_STARTED', {
+        'moduleContextFileName':'serial-util1146216{974}2772610436.ser',
+        'moduleName':'someTestModule'}),
+    ('TEST_RUN_STARTED', {'testCount': 2, 'runName': 'com.android.UnitTests'}),
+    ('TEST_STARTED', {'start_time':52, 'className':'someClassName',
+                      'testName':'someTestName'}),
+    ('TEST_ENDED', {'end_time':1048, 'className':'someClassName',
+                    'testName':'someTestName'}),
+    ('TEST_STARTED', {'start_time':48, 'className':'someClassName2',
+                      'testName':'someTestName2'}),
+    ('TEST_FAILED', {'className':'someClassName2', 'testName':'someTestName2',
+                     'trace': 'someTrace'}),
+    ('TEST_ENDED', {'end_time':9876450, 'className':'someClassName2',
+                    'testName':'someTestName2'}),
+    ('TEST_RUN_ENDED', {}),
+    ('TEST_MODULE_ENDED', {'foo': 'bar'}),
+]
+
+EVENTS_RUN_FAILURE = [
+    ('TEST_MODULE_STARTED', {
+        'moduleContextFileName': 'serial-util11462169742772610436.ser',
+        'moduleName': 'someTestModule'}),
+    ('TEST_RUN_STARTED', {'testCount': 2, 'runName': 'com.android.UnitTests'}),
+    ('TEST_STARTED', {'start_time':10, 'className': 'someClassName',
+                      'testName':'someTestName'}),
+    ('TEST_RUN_FAILED', {'reason': 'someRunFailureReason'})
+]
+
+
+EVENTS_INVOCATION_FAILURE = [
+    ('TEST_RUN_STARTED', {'testCount': None, 'runName': 'com.android.UnitTests'}),
+    ('INVOCATION_FAILED', {'cause': 'someInvocationFailureReason'})
+]
+
+EVENTS_MISSING_TEST_RUN_STARTED_EVENT = [
+    ('TEST_STARTED', {'start_time':52, 'className':'someClassName',
+                      'testName':'someTestName'}),
+    ('TEST_ENDED', {'end_time':1048, 'className':'someClassName',
+                    'testName':'someTestName'}),
+]
+
+EVENTS_NOT_BALANCED_BEFORE_RAISE = [
+    ('TEST_MODULE_STARTED', {
+        'moduleContextFileName':'serial-util1146216{974}2772610436.ser',
+        'moduleName':'someTestModule'}),
+    ('TEST_RUN_STARTED', {'testCount': 2, 'runName': 'com.android.UnitTests'}),
+    ('TEST_STARTED', {'start_time':10, 'className':'someClassName',
+                      'testName':'someTestName'}),
+    ('TEST_ENDED', {'end_time':18, 'className':'someClassName',
+                    'testName':'someTestName'}),
+    ('TEST_STARTED', {'start_time':19, 'className':'someClassName',
+                      'testName':'someTestName'}),
+    ('TEST_FAILED', {'className':'someClassName2', 'testName':'someTestName2',
+                     'trace': 'someTrace'}),
+]
+
+EVENTS_IGNORE = [
+    ('TEST_MODULE_STARTED', {
+        'moduleContextFileName':'serial-util1146216{974}2772610436.ser',
+        'moduleName':'someTestModule'}),
+    ('TEST_RUN_STARTED', {'testCount': 2, 'runName': 'com.android.UnitTests'}),
+    ('TEST_STARTED', {'start_time':8, 'className':'someClassName',
+                      'testName':'someTestName'}),
+    ('TEST_ENDED', {'end_time':18, 'className':'someClassName',
+                    'testName':'someTestName'}),
+    ('TEST_STARTED', {'start_time':28, 'className':'someClassName2',
+                      'testName':'someTestName2'}),
+    ('TEST_IGNORED', {'className':'someClassName2', 'testName':'someTestName2',
+                      'trace': 'someTrace'}),
+    ('TEST_ENDED', {'end_time':90, 'className':'someClassName2',
+                    'testName':'someTestName2'}),
+    ('TEST_RUN_ENDED', {}),
+    ('TEST_MODULE_ENDED', {'foo': 'bar'}),
+]
+
+EVENTS_WITH_PERF_INFO = [
+    ('TEST_MODULE_STARTED', {
+        'moduleContextFileName':'serial-util1146216{974}2772610436.ser',
+        'moduleName':'someTestModule'}),
+    ('TEST_RUN_STARTED', {'testCount': 2, 'runName': 'com.android.UnitTests'}),
+    ('TEST_STARTED', {'start_time':52, 'className':'someClassName',
+                      'testName':'someTestName'}),
+    ('TEST_ENDED', {'end_time':1048, 'className':'someClassName',
+                    'testName':'someTestName'}),
+    ('TEST_STARTED', {'start_time':48, 'className':'someClassName2',
+                      'testName':'someTestName2'}),
+    ('TEST_FAILED', {'className':'someClassName2', 'testName':'someTestName2',
+                     'trace': 'someTrace'}),
+    ('TEST_ENDED', {'end_time':9876450, 'className':'someClassName2',
+                    'testName':'someTestName2', 'cpu_time':'1234.1234(ns)',
+                    'real_time':'5678.5678(ns)', 'iterations':'6666'}),
+    ('TEST_STARTED', {'start_time':10, 'className':'someClassName3',
+                      'testName':'someTestName3'}),
+    ('TEST_ENDED', {'end_time':70, 'className':'someClassName3',
+                    'testName':'someTestName3', 'additional_info_min':'102773',
+                    'additional_info_mean':'105973', 'additional_info_median':'103778'}),
+    ('TEST_RUN_ENDED', {}),
+    ('TEST_MODULE_ENDED', {'foo': 'bar'}),
+]
+
+class EventHandlerUnittests(unittest.TestCase):
+    """Unit tests for event_handler.py"""
+
+    def setUp(self):
+        reload(e_h)
+        self.mock_reporter = mock.Mock()
+        self.fake_eh = e_h.EventHandler(self.mock_reporter,
+                                        atf_tr.AtestTradefedTestRunner.NAME)
+
+    def tearDown(self):
+        mock.patch.stopall()
+
+    def test_process_event_normal_results(self):
+        """Test process_event method for normal test results."""
+        for name, data in EVENTS_NORMAL:
+            self.fake_eh.process_event(name, data)
+        call1 = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName#someTestName',
+            status=test_runner_base.PASSED_STATUS,
+            details=None,
+            test_count=1,
+            test_time='(996ms)',
+            runner_total=None,
+            group_total=2,
+            additional_info={},
+            test_run_name='com.android.UnitTests'
+        ))
+        call2 = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName2#someTestName2',
+            status=test_runner_base.FAILED_STATUS,
+            details='someTrace',
+            test_count=2,
+            test_time='(2h44m36.402s)',
+            runner_total=None,
+            group_total=2,
+            additional_info={},
+            test_run_name='com.android.UnitTests'
+        ))
+        self.mock_reporter.process_test_result.assert_has_calls([call1, call2])
+
+    def test_process_event_run_failure(self):
+        """Test process_event method run failure."""
+        for name, data in EVENTS_RUN_FAILURE:
+            self.fake_eh.process_event(name, data)
+        call = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName#someTestName',
+            status=test_runner_base.ERROR_STATUS,
+            details='someRunFailureReason',
+            test_count=1,
+            test_time='',
+            runner_total=None,
+            group_total=2,
+            additional_info={},
+            test_run_name='com.android.UnitTests'
+        ))
+        self.mock_reporter.process_test_result.assert_has_calls([call])
+
+    def test_process_event_invocation_failure(self):
+        """Test process_event method with invocation failure."""
+        for name, data in EVENTS_INVOCATION_FAILURE:
+            self.fake_eh.process_event(name, data)
+        call = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name=None,
+            test_name=None,
+            status=test_runner_base.ERROR_STATUS,
+            details='someInvocationFailureReason',
+            test_count=0,
+            test_time='',
+            runner_total=None,
+            group_total=None,
+            additional_info={},
+            test_run_name='com.android.UnitTests'
+        ))
+        self.mock_reporter.process_test_result.assert_has_calls([call])
+
+    def test_process_event_missing_test_run_started_event(self):
+        """Test process_event method for normal test results."""
+        for name, data in EVENTS_MISSING_TEST_RUN_STARTED_EVENT:
+            self.fake_eh.process_event(name, data)
+        call = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name=None,
+            test_name='someClassName#someTestName',
+            status=test_runner_base.PASSED_STATUS,
+            details=None,
+            test_count=1,
+            test_time='(996ms)',
+            runner_total=None,
+            group_total=None,
+            additional_info={},
+            test_run_name=None
+        ))
+        self.mock_reporter.process_test_result.assert_has_calls([call])
+
+    # pylint: disable=protected-access
+    def test_process_event_not_balanced(self):
+        """Test process_event method with start/end event name not balanced."""
+        for name, data in EVENTS_NOT_BALANCED_BEFORE_RAISE:
+            self.fake_eh.process_event(name, data)
+        call = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName#someTestName',
+            status=test_runner_base.PASSED_STATUS,
+            details=None,
+            test_count=1,
+            test_time='(8ms)',
+            runner_total=None,
+            group_total=2,
+            additional_info={},
+            test_run_name='com.android.UnitTests'
+        ))
+        self.mock_reporter.process_test_result.assert_has_calls([call])
+        # Event pair: TEST_STARTED -> TEST_RUN_ENDED
+        # It should raise TradeFedExitError in _check_events_are_balanced()
+        name = 'TEST_RUN_ENDED'
+        data = {}
+        self.assertRaises(e_h.EventHandleError,
+                          self.fake_eh._check_events_are_balanced,
+                          name, self.mock_reporter)
+        # Event pair: TEST_RUN_STARTED -> TEST_MODULE_ENDED
+        # It should raise TradeFedExitError in _check_events_are_balanced()
+        name = 'TEST_MODULE_ENDED'
+        data = {'foo': 'bar'}
+        self.assertRaises(e_h.EventHandleError,
+                          self.fake_eh._check_events_are_balanced,
+                          name, self.mock_reporter)
+
+    def test_process_event_ignore(self):
+        """Test _process_event method for normal test results."""
+        for name, data in EVENTS_IGNORE:
+            self.fake_eh.process_event(name, data)
+        call1 = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName#someTestName',
+            status=test_runner_base.PASSED_STATUS,
+            details=None,
+            test_count=1,
+            test_time='(10ms)',
+            runner_total=None,
+            group_total=2,
+            additional_info={},
+            test_run_name='com.android.UnitTests'
+        ))
+        call2 = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName2#someTestName2',
+            status=test_runner_base.IGNORED_STATUS,
+            details=None,
+            test_count=2,
+            test_time='(62ms)',
+            runner_total=None,
+            group_total=2,
+            additional_info={},
+            test_run_name='com.android.UnitTests'
+        ))
+        self.mock_reporter.process_test_result.assert_has_calls([call1, call2])
+
+    def test_process_event_with_additional_info(self):
+        """Test process_event method with perf information."""
+        for name, data in EVENTS_WITH_PERF_INFO:
+            self.fake_eh.process_event(name, data)
+        call1 = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName#someTestName',
+            status=test_runner_base.PASSED_STATUS,
+            details=None,
+            test_count=1,
+            test_time='(996ms)',
+            runner_total=None,
+            group_total=2,
+            additional_info={},
+            test_run_name='com.android.UnitTests'
+        ))
+
+        test_additional_info = {'cpu_time':'1234.1234(ns)', 'real_time':'5678.5678(ns)',
+                                'iterations':'6666'}
+        call2 = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName2#someTestName2',
+            status=test_runner_base.FAILED_STATUS,
+            details='someTrace',
+            test_count=2,
+            test_time='(2h44m36.402s)',
+            runner_total=None,
+            group_total=2,
+            additional_info=test_additional_info,
+            test_run_name='com.android.UnitTests'
+        ))
+
+        test_additional_info2 = {'additional_info_min':'102773',
+                                 'additional_info_mean':'105973',
+                                 'additional_info_median':'103778'}
+        call3 = mock.call(test_runner_base.TestResult(
+            runner_name=atf_tr.AtestTradefedTestRunner.NAME,
+            group_name='someTestModule',
+            test_name='someClassName3#someTestName3',
+            status=test_runner_base.PASSED_STATUS,
+            details=None,
+            test_count=3,
+            test_time='(60ms)',
+            runner_total=None,
+            group_total=2,
+            additional_info=test_additional_info2,
+            test_run_name='com.android.UnitTests'
+        ))
+        self.mock_reporter.process_test_result.assert_has_calls([call1, call2, call3])
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_runners/example_test_runner.py b/atest/test_runners/example_test_runner.py
new file mode 100644
index 0000000..dc18112
--- /dev/null
+++ b/atest/test_runners/example_test_runner.py
@@ -0,0 +1,77 @@
+# Copyright 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.
+
+"""
+Example test runner class.
+"""
+
+# pylint: disable=import-error
+import test_runner_base
+
+
+class ExampleTestRunner(test_runner_base.TestRunnerBase):
+    """Base Test Runner class."""
+    NAME = 'ExampleTestRunner'
+    EXECUTABLE = 'echo'
+    _RUN_CMD = '{exe} ExampleTestRunner - test:{test}'
+    _BUILD_REQ = set()
+
+    def run_tests(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos.
+
+        Args:
+            test_infos: List of TestInfo.
+            extra_args: Dict of extra args to add to test run.
+            reporter: An instance of result_report.ResultReporter
+        """
+        run_cmds = self.generate_run_commands(test_infos, extra_args)
+        for run_cmd in run_cmds:
+            super(ExampleTestRunner, self).run(run_cmd)
+
+    def host_env_check(self):
+        """Check that host env has everything we need.
+
+        We actually can assume the host env is fine because we have the same
+        requirements that atest has. Update this to check for android env vars
+        if that changes.
+        """
+        pass
+
+    def get_test_runner_build_reqs(self):
+        """Return the build requirements.
+
+        Returns:
+            Set of build targets.
+        """
+        return set()
+
+    # pylint: disable=unused-argument
+    def generate_run_commands(self, test_infos, extra_args, port=None):
+        """Generate a list of run commands from TestInfos.
+
+        Args:
+            test_infos: A set of TestInfo instances.
+            extra_args: A Dict of extra args to append.
+            port: Optional. An int of the port number to send events to.
+                  Subprocess reporter in TF won't try to connect if it's None.
+
+        Returns:
+            A list of run commands to run the tests.
+        """
+        run_cmds = []
+        for test_info in test_infos:
+            run_cmd_dict = {'exe': self.EXECUTABLE,
+                            'test': test_info.test_name}
+            run_cmds.extend(self._RUN_CMD.format(**run_cmd_dict))
+        return run_cmds
diff --git a/atest/test_runners/regression_test_runner.py b/atest/test_runners/regression_test_runner.py
new file mode 100644
index 0000000..078040a
--- /dev/null
+++ b/atest/test_runners/regression_test_runner.py
@@ -0,0 +1,91 @@
+# Copyright 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.
+
+"""
+Regression Detection test runner class.
+"""
+
+# pylint: disable=import-error
+import constants
+from test_runners import test_runner_base
+
+
+class RegressionTestRunner(test_runner_base.TestRunnerBase):
+    """Regression Test Runner class."""
+    NAME = 'RegressionTestRunner'
+    EXECUTABLE = 'tradefed.sh'
+    _RUN_CMD = '{exe} run commandAndExit regression -n {args}'
+    _BUILD_REQ = {'tradefed-core', constants.ATEST_TF_MODULE}
+
+    def __init__(self, results_dir):
+        """Init stuff for base class."""
+        super(RegressionTestRunner, self).__init__(results_dir)
+        self.run_cmd_dict = {'exe': self.EXECUTABLE,
+                             'args': ''}
+
+    # pylint: disable=unused-argument
+    def run_tests(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos.
+
+        Args:
+            test_infos: List of TestInfo.
+            extra_args: Dict of args to add to regression detection test run.
+            reporter: A ResultReporter instance.
+
+        Returns:
+            Return code of the process for running tests.
+        """
+        run_cmds = self.generate_run_commands(test_infos, extra_args)
+        proc = super(RegressionTestRunner, self).run(run_cmds[0],
+                                                     output_to_stdout=True)
+        proc.wait()
+        return proc.returncode
+
+    def host_env_check(self):
+        """Check that host env has everything we need.
+
+        We actually can assume the host env is fine because we have the same
+        requirements that atest has. Update this to check for android env vars
+        if that changes.
+        """
+        pass
+
+    def get_test_runner_build_reqs(self):
+        """Return the build requirements.
+
+        Returns:
+            Set of build targets.
+        """
+        return self._BUILD_REQ
+
+    # pylint: disable=unused-argument
+    def generate_run_commands(self, test_infos, extra_args, port=None):
+        """Generate a list of run commands from TestInfos.
+
+        Args:
+            test_infos: A set of TestInfo instances.
+            extra_args: A Dict of extra args to append.
+            port: Optional. An int of the port number to send events to.
+                  Subprocess reporter in TF won't try to connect if it's None.
+
+        Returns:
+            A list that contains the string of atest tradefed run command.
+            Only one command is returned.
+        """
+        pre = extra_args.pop(constants.PRE_PATCH_FOLDER)
+        post = extra_args.pop(constants.POST_PATCH_FOLDER)
+        args = ['--pre-patch-metrics', pre, '--post-patch-metrics', post]
+        self.run_cmd_dict['args'] = ' '.join(args)
+        run_cmd = self._RUN_CMD.format(**self.run_cmd_dict)
+        return [run_cmd]
diff --git a/atest/test_runners/robolectric_test_runner.py b/atest/test_runners/robolectric_test_runner.py
new file mode 100644
index 0000000..fa34149
--- /dev/null
+++ b/atest/test_runners/robolectric_test_runner.py
@@ -0,0 +1,256 @@
+# Copyright 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.
+
+"""
+Robolectric test runner class.
+
+This test runner will be short lived, once robolectric support v2 is in, then
+robolectric tests will be invoked through AtestTFTestRunner.
+"""
+
+import json
+import logging
+import os
+import re
+import tempfile
+import time
+
+from functools import partial
+
+# pylint: disable=import-error
+import atest_utils
+import constants
+
+from event_handler import EventHandler
+from test_runners import test_runner_base
+
+POLL_FREQ_SECS = 0.1
+# A pattern to match event like below
+#TEST_FAILED {'className':'SomeClass', 'testName':'SomeTestName',
+#            'trace':'{"trace":"AssertionError: <true> is equal to <false>\n
+#               at FailureStrategy.fail(FailureStrategy.java:24)\n
+#               at FailureStrategy.fail(FailureStrategy.java:20)\n"}\n\n
+EVENT_RE = re.compile(r'^(?P<event_name>[A-Z_]+) (?P<json_data>{(.\r*|\n)*})(?:\n|$)')
+
+
+class RobolectricTestRunner(test_runner_base.TestRunnerBase):
+    """Robolectric Test Runner class."""
+    NAME = 'RobolectricTestRunner'
+    # We don't actually use EXECUTABLE because we're going to use
+    # atest_utils.build to kick off the test but if we don't set it, the base
+    # class will raise an exception.
+    EXECUTABLE = 'make'
+
+    # pylint: disable=useless-super-delegation
+    def __init__(self, results_dir, **kwargs):
+        """Init stuff for robolectric runner class."""
+        super(RobolectricTestRunner, self).__init__(results_dir, **kwargs)
+        self.is_verbose = logging.getLogger().isEnabledFor(logging.DEBUG)
+
+    def run_tests(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos. See base class for more.
+
+        Args:
+            test_infos: A list of TestInfos.
+            extra_args: Dict of extra args to add to test run.
+            reporter: An instance of result_report.ResultReporter.
+
+        Returns:
+            0 if tests succeed, non-zero otherwise.
+        """
+        if os.getenv(test_runner_base.OLD_OUTPUT_ENV_VAR):
+            return self.run_tests_raw(test_infos, extra_args, reporter)
+        return self.run_tests_pretty(test_infos, extra_args, reporter)
+
+    def run_tests_raw(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos with raw output.
+
+        Args:
+            test_infos: List of TestInfo.
+            extra_args: Dict of extra args to add to test run.
+            reporter: A ResultReporter Instance.
+
+        Returns:
+            0 if tests succeed, non-zero otherwise.
+        """
+        reporter.register_unsupported_runner(self.NAME)
+        ret_code = constants.EXIT_CODE_SUCCESS
+        for test_info in test_infos:
+            full_env_vars = self._get_full_build_environ(test_info,
+                                                         extra_args)
+            run_cmd = self.generate_run_commands([test_info], extra_args)[0]
+            subproc = self.run(run_cmd,
+                               output_to_stdout=self.is_verbose,
+                               env_vars=full_env_vars)
+            ret_code |= self.wait_for_subprocess(subproc)
+        return ret_code
+
+    def run_tests_pretty(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos with pretty output mode.
+
+        Args:
+            test_infos: List of TestInfo.
+            extra_args: Dict of extra args to add to test run.
+            reporter: A ResultReporter Instance.
+
+        Returns:
+            0 if tests succeed, non-zero otherwise.
+        """
+        ret_code = constants.EXIT_CODE_SUCCESS
+        for test_info in test_infos:
+            # Create a temp communication file.
+            with tempfile.NamedTemporaryFile(mode='w+r',
+                                             dir=self.results_dir) as event_file:
+                # Prepare build environment parameter.
+                full_env_vars = self._get_full_build_environ(test_info,
+                                                             extra_args,
+                                                             event_file)
+                run_cmd = self.generate_run_commands([test_info], extra_args)[0]
+                subproc = self.run(run_cmd,
+                                   output_to_stdout=self.is_verbose,
+                                   env_vars=full_env_vars)
+                event_handler = EventHandler(reporter, self.NAME)
+                # Start polling.
+                self.handle_subprocess(subproc, partial(self._exec_with_robo_polling,
+                                                        event_file,
+                                                        subproc,
+                                                        event_handler))
+                ret_code |= self.wait_for_subprocess(subproc)
+        return ret_code
+
+    def _get_full_build_environ(self, test_info=None, extra_args=None, event_file=None):
+        """Helper to get full build environment.
+
+       Args:
+           test_info: TestInfo object.
+           extra_args: Dict of extra args to add to test run.
+           event_file: A file-like object that can be used as a temporary storage area.
+       """
+        full_env_vars = os.environ.copy()
+        env_vars = self.generate_env_vars(test_info,
+                                          extra_args,
+                                          event_file)
+        full_env_vars.update(env_vars)
+        return full_env_vars
+
+    def _exec_with_robo_polling(self, communication_file, robo_proc, event_handler):
+        """Polling data from communication file
+
+        Polling data from communication file. Exit when communication file
+        is empty and subprocess ended.
+
+        Args:
+            communication_file: A monitored communication file.
+            robo_proc: The build process.
+            event_handler: A file-like object storing the events of robolectric tests.
+        """
+        buf = ''
+        while True:
+            # Make sure that ATest gets content from current position.
+            communication_file.seek(0, 1)
+            data = communication_file.read()
+            buf += data
+            reg = re.compile(r'(.|\n)*}\n\n')
+            if not reg.match(buf) or data == '':
+                if robo_proc.poll() is not None:
+                    logging.debug('Build process exited early')
+                    return
+                time.sleep(POLL_FREQ_SECS)
+            else:
+                # Read all new data and handle it at one time.
+                for event in re.split(r'\n\n', buf):
+                    match = EVENT_RE.match(event)
+                    if match:
+                        try:
+                            event_data = json.loads(match.group('json_data'),
+                                                    strict=False)
+                        except ValueError:
+                            # Parse event fail, continue to parse next one.
+                            logging.debug('"%s" is not valid json format.',
+                                          match.group('json_data'))
+                            continue
+                        event_name = match.group('event_name')
+                        event_handler.process_event(event_name, event_data)
+                buf = ''
+
+    @staticmethod
+    def generate_env_vars(test_info, extra_args, event_file=None):
+        """Turn the args into env vars.
+
+        Robolectric tests specify args through env vars, so look for class
+        filters and debug args to apply to the env.
+
+        Args:
+            test_info: TestInfo class that holds the class filter info.
+            extra_args: Dict of extra args to apply for test run.
+            event_file: A file-like object storing the events of robolectric tests.
+
+        Returns:
+            Dict of env vars to pass into invocation.
+        """
+        env_var = {}
+        for arg in extra_args:
+            if constants.WAIT_FOR_DEBUGGER == arg:
+                env_var['DEBUG_ROBOLECTRIC'] = 'true'
+                continue
+        filters = test_info.data.get(constants.TI_FILTER)
+        if filters:
+            robo_filter = next(iter(filters))
+            env_var['ROBOTEST_FILTER'] = robo_filter.class_name
+            if robo_filter.methods:
+                logging.debug('method filtering not supported for robolectric '
+                              'tests yet.')
+        if event_file:
+            env_var['EVENT_FILE_ROBOLECTRIC'] = event_file.name
+        return env_var
+
+    def host_env_check(self):
+        """Check that host env has everything we need.
+
+        We actually can assume the host env is fine because we have the same
+        requirements that atest has. Update this to check for android env vars
+        if that changes.
+        """
+        pass
+
+    def get_test_runner_build_reqs(self):
+        """Return the build requirements.
+
+        Returns:
+            Set of build targets.
+        """
+        return set()
+
+    # pylint: disable=unused-argument
+    def generate_run_commands(self, test_infos, extra_args, port=None):
+        """Generate a list of run commands from TestInfos.
+
+        Args:
+            test_infos: A set of TestInfo instances.
+            extra_args: A Dict of extra args to append.
+            port: Optional. An int of the port number to send events to.
+                  Subprocess reporter in TF won't try to connect if it's None.
+
+        Returns:
+            A list of run commands to run the tests.
+        """
+        run_cmds = []
+        for test_info in test_infos:
+            robo_command = atest_utils.get_build_cmd() + [str(test_info.test_name)]
+            run_cmd = ' '.join(x for x in robo_command)
+            if constants.DRY_RUN in extra_args:
+                run_cmd = run_cmd.replace(
+                    os.environ.get(constants.ANDROID_BUILD_TOP) + os.sep, '')
+            run_cmds.append(run_cmd)
+        return run_cmds
diff --git a/atest/test_runners/robolectric_test_runner_unittest.py b/atest/test_runners/robolectric_test_runner_unittest.py
new file mode 100755
index 0000000..46164f0
--- /dev/null
+++ b/atest/test_runners/robolectric_test_runner_unittest.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+"""Unittests for robolectric_test_runner."""
+
+import json
+import unittest
+import subprocess
+import tempfile
+import mock
+
+import event_handler
+# pylint: disable=import-error
+from test_finders import test_info
+from test_runners import robolectric_test_runner
+
+# pylint: disable=protected-access
+class RobolectricTestRunnerUnittests(unittest.TestCase):
+    """Unit tests for robolectric_test_runner.py"""
+
+    def setUp(self):
+        self.polling_time = robolectric_test_runner.POLL_FREQ_SECS
+        self.suite_tr = robolectric_test_runner.RobolectricTestRunner(results_dir='')
+
+    def tearDown(self):
+        mock.patch.stopall()
+
+    @mock.patch.object(robolectric_test_runner.RobolectricTestRunner, 'run')
+    def test_run_tests_raw(self, mock_run):
+        """Test run_tests_raw method."""
+        test_infos = [test_info.TestInfo("Robo1",
+                                         "RobolectricTestRunner",
+                                         ["RoboTest"])]
+        extra_args = []
+        mock_subproc = mock.Mock()
+        mock_run.return_value = mock_subproc
+        mock_subproc.returncode = 0
+        mock_reporter = mock.Mock()
+        # Test Build Pass
+        self.assertEqual(
+            0,
+            self.suite_tr.run_tests_raw(test_infos, extra_args, mock_reporter))
+        # Test Build Fail
+        mock_subproc.returncode = 1
+        self.assertNotEqual(
+            0,
+            self.suite_tr.run_tests_raw(test_infos, extra_args, mock_reporter))
+
+    @mock.patch.object(event_handler.EventHandler, 'process_event')
+    def test_exec_with_robo_polling_complete_information(self, mock_pe):
+        """Test _exec_with_robo_polling method."""
+        event_name = 'TEST_STARTED'
+        event_data = {'className':'SomeClass', 'testName':'SomeTestName'}
+
+        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=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)
+
+    @mock.patch.object(event_handler.EventHandler, 'process_event')
+    def test_exec_with_robo_polling_with_partial_info(self, mock_pe):
+        """Test _exec_with_robo_polling method."""
+        event_name = 'TEST_STARTED'
+        event1 = '{"className":"SomeClass","test'
+        event2 = 'Name":"SomeTestName"}\n\n'
+        data1 = '%s %s'%(event_name, event1)
+        data2 = event2
+        event_file = tempfile.NamedTemporaryFile(mode='w+r', delete=True)
+        subprocess.Popen("echo -n '%s' >> %s" %(data1, event_file.name), shell=True)
+        robo_proc = subprocess.Popen("echo '%s' >> %s && sleep %s"
+                                     %(data2,
+                                       event_file.name,
+                                       str(self.polling_time*5)),
+                                     shell=True)
+        self.suite_tr. _exec_with_robo_polling(event_file, robo_proc, mock_pe)
+        calls = [mock.call.process_event(event_name,
+                                         json.loads(event1 + event2))]
+        mock_pe.assert_has_calls(calls)
+
+    @mock.patch.object(event_handler.EventHandler, 'process_event')
+    def test_exec_with_robo_polling_with_fail_stacktrace(self, mock_pe):
+        """Test _exec_with_robo_polling method."""
+        event_name = 'TEST_FAILED'
+        event_data = {'className':'SomeClass', 'testName':'SomeTestName',
+                      'trace':'{"trace":"AssertionError: <true> is equal to <false>\n'
+                              'at FailureStrategy.fail(FailureStrategy.java:24)\n'
+                              '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)
+        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)
+
+    @mock.patch.object(event_handler.EventHandler, 'process_event')
+    def test_exec_with_robo_polling_with_multi_event(self, mock_pe):
+        """Test _exec_with_robo_polling method."""
+        event_file = tempfile.NamedTemporaryFile(mode='w+r', delete=True)
+        events = [
+            ('TEST_MODULE_STARTED', {
+                'moduleContextFileName':'serial-util1146216{974}2772610436.ser',
+                'moduleName':'someTestModule'}),
+            ('TEST_RUN_STARTED', {'testCount': 2}),
+            ('TEST_STARTED', {'start_time':52, 'className':'someClassName',
+                              'testName':'someTestName'}),
+            ('TEST_ENDED', {'end_time':1048, 'className':'someClassName',
+                            'testName':'someTestName'}),
+            ('TEST_STARTED', {'start_time':48, 'className':'someClassName2',
+                              'testName':'someTestName2'}),
+            ('TEST_FAILED', {'className':'someClassName2', 'testName':'someTestName2',
+                             'trace': 'someTrace'}),
+            ('TEST_ENDED', {'end_time':9876450, 'className':'someClassName2',
+                            'testName':'someTestName2'}),
+            ('TEST_RUN_ENDED', {}),
+            ('TEST_MODULE_ENDED', {'foo': 'bar'}),]
+        data = ''
+        for event in events:
+            data += '%s %s\n\n'%(event[0], json.dumps(event[1]))
+
+        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)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_runners/suite_plan_test_runner.py b/atest/test_runners/suite_plan_test_runner.py
new file mode 100644
index 0000000..9ba8233
--- /dev/null
+++ b/atest/test_runners/suite_plan_test_runner.py
@@ -0,0 +1,125 @@
+# Copyright 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.
+
+"""
+SUITE Tradefed test runner class.
+"""
+
+import copy
+import logging
+
+# pylint: disable=import-error
+from test_runners import atest_tf_test_runner
+import atest_utils
+import constants
+
+
+class SuitePlanTestRunner(atest_tf_test_runner.AtestTradefedTestRunner):
+    """Suite Plan Test Runner class."""
+    NAME = 'SuitePlanTestRunner'
+    EXECUTABLE = '%s-tradefed'
+    _RUN_CMD = ('{exe} run commandAndExit {test} {args}')
+
+    def __init__(self, results_dir, **kwargs):
+        """Init stuff for suite tradefed runner class."""
+        super(SuitePlanTestRunner, self).__init__(results_dir, **kwargs)
+        self.run_cmd_dict = {'exe': '',
+                             'test': '',
+                             'args': ''}
+
+    def get_test_runner_build_reqs(self):
+        """Return the build requirements.
+
+        Returns:
+            Set of build targets.
+        """
+        build_req = set()
+        build_req |= super(SuitePlanTestRunner,
+                           self).get_test_runner_build_reqs()
+        return build_req
+
+    def run_tests(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos.
+        Args:
+            test_infos: List of TestInfo.
+            extra_args: Dict of extra args to add to test run.
+            reporter: An instance of result_report.ResultReporter.
+
+        Returns:
+            Return code of the process for running tests.
+        """
+        reporter.register_unsupported_runner(self.NAME)
+        run_cmds = self.generate_run_commands(test_infos, extra_args)
+        ret_code = constants.EXIT_CODE_SUCCESS
+        for run_cmd in run_cmds:
+            proc = super(SuitePlanTestRunner, self).run(run_cmd,
+                                                        output_to_stdout=True)
+            ret_code |= self.wait_for_subprocess(proc)
+        return ret_code
+
+    def _parse_extra_args(self, extra_args):
+        """Convert the extra args into something *ts-tf can understand.
+
+        We want to transform the top-level args from atest into specific args
+        that *ts-tradefed supports. The only arg we take as is
+        EXTRA_ARG since that is what the user intentionally wants to pass to
+        the test runner.
+
+        Args:
+            extra_args: Dict of args
+
+        Returns:
+            List of args to append.
+        """
+        args_to_append = []
+        args_not_supported = []
+        for arg in extra_args:
+            if constants.SERIAL == arg:
+                args_to_append.append('--serial')
+                args_to_append.append(extra_args[arg])
+                continue
+            if constants.CUSTOM_ARGS == arg:
+                args_to_append.extend(extra_args[arg])
+                continue
+            if constants.DRY_RUN == arg:
+                continue
+            args_not_supported.append(arg)
+        if args_not_supported:
+            logging.info('%s does not support the following args: %s',
+                         self.EXECUTABLE, args_not_supported)
+        return args_to_append
+
+    # pylint: disable=arguments-differ
+    def generate_run_commands(self, test_infos, extra_args):
+        """Generate a list of run commands from TestInfos.
+
+        Args:
+            test_infos: List of TestInfo tests to run.
+            extra_args: Dict of extra args to add to test run.
+
+        Returns:
+            A List of strings that contains the run command
+            which *ts-tradefed supports.
+        """
+        cmds = []
+        args = []
+        args.extend(self._parse_extra_args(extra_args))
+        args.extend(atest_utils.get_result_server_args())
+        for test_info in test_infos:
+            cmd_dict = copy.deepcopy(self.run_cmd_dict)
+            cmd_dict['test'] = test_info.test_name
+            cmd_dict['args'] = ' '.join(args)
+            cmd_dict['exe'] = self.EXECUTABLE % test_info.suite
+            cmds.append(self._RUN_CMD.format(**cmd_dict))
+        return cmds
diff --git a/atest/test_runners/suite_plan_test_runner_unittest.py b/atest/test_runners/suite_plan_test_runner_unittest.py
new file mode 100755
index 0000000..857452e
--- /dev/null
+++ b/atest/test_runners/suite_plan_test_runner_unittest.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+"""Unittests for test_suite_test_runner."""
+
+import unittest
+import mock
+
+# pylint: disable=import-error
+import suite_plan_test_runner
+import unittest_utils
+from test_finders import test_info
+
+
+# pylint: disable=protected-access
+class SuitePlanTestRunnerUnittests(unittest.TestCase):
+    """Unit tests for test_suite_test_runner.py"""
+
+    def setUp(self):
+        self.suite_tr = suite_plan_test_runner.SuitePlanTestRunner(results_dir='')
+
+    def tearDown(self):
+        mock.patch.stopall()
+
+    @mock.patch('atest_utils.get_result_server_args')
+    def test_generate_run_commands(self, mock_resultargs):
+        """Test _generate_run_command method.
+        Strategy:
+            suite_name: cts --> run_cmd: cts-tradefed run commandAndExit cts
+            suite_name: cts-common --> run_cmd:
+                                cts-tradefed run commandAndExit cts-common
+        """
+        test_infos = set()
+        suite_name = 'cts'
+        t_info = test_info.TestInfo(suite_name,
+                                    suite_plan_test_runner.SuitePlanTestRunner.NAME,
+                                    {suite_name},
+                                    suite=suite_name)
+        test_infos.add(t_info)
+
+        # Basic Run Cmd
+        run_cmd = []
+        exe_cmd = suite_plan_test_runner.SuitePlanTestRunner.EXECUTABLE % suite_name
+        run_cmd.append(suite_plan_test_runner.SuitePlanTestRunner._RUN_CMD.format(
+            exe=exe_cmd,
+            test=suite_name,
+            args=''))
+        mock_resultargs.return_value = []
+        unittest_utils.assert_strict_equal(
+            self,
+            self.suite_tr.generate_run_commands(test_infos, ''),
+            run_cmd)
+
+        # Run cmd with --serial LG123456789.
+        run_cmd = []
+        run_cmd.append(suite_plan_test_runner.SuitePlanTestRunner._RUN_CMD.format(
+            exe=exe_cmd,
+            test=suite_name,
+            args='--serial LG123456789'))
+        unittest_utils.assert_strict_equal(
+            self,
+            self.suite_tr.generate_run_commands(test_infos, {'SERIAL':'LG123456789'}),
+            run_cmd)
+
+        test_infos = set()
+        suite_name = 'cts-common'
+        suite = 'cts'
+        t_info = test_info.TestInfo(suite_name,
+                                    suite_plan_test_runner.SuitePlanTestRunner.NAME,
+                                    {suite_name},
+                                    suite=suite)
+        test_infos.add(t_info)
+
+        # Basic Run Cmd
+        run_cmd = []
+        exe_cmd = suite_plan_test_runner.SuitePlanTestRunner.EXECUTABLE % suite
+        run_cmd.append(suite_plan_test_runner.SuitePlanTestRunner._RUN_CMD.format(
+            exe=exe_cmd,
+            test=suite_name,
+            args=''))
+        mock_resultargs.return_value = []
+        unittest_utils.assert_strict_equal(
+            self,
+            self.suite_tr.generate_run_commands(test_infos, ''),
+            run_cmd)
+
+        # Run cmd with --serial LG123456789.
+        run_cmd = []
+        run_cmd.append(suite_plan_test_runner.SuitePlanTestRunner._RUN_CMD.format(
+            exe=exe_cmd,
+            test=suite_name,
+            args='--serial LG123456789'))
+        unittest_utils.assert_strict_equal(
+            self,
+            self.suite_tr.generate_run_commands(test_infos, {'SERIAL':'LG123456789'}),
+            run_cmd)
+
+    @mock.patch('subprocess.Popen')
+    @mock.patch.object(suite_plan_test_runner.SuitePlanTestRunner, 'run')
+    @mock.patch.object(suite_plan_test_runner.SuitePlanTestRunner,
+                       'generate_run_commands')
+    def test_run_tests(self, _mock_gen_cmd, _mock_run, _mock_popen):
+        """Test run_tests method."""
+        test_infos = []
+        extra_args = []
+        mock_reporter = mock.Mock()
+        _mock_gen_cmd.return_value = ["cmd1", "cmd2"]
+        # Test Build Pass
+        _mock_popen.return_value.returncode = 0
+        self.assertEqual(
+            0,
+            self.suite_tr.run_tests(test_infos, extra_args, mock_reporter))
+
+        # Test Build Pass
+        _mock_popen.return_value.returncode = 1
+        self.assertNotEqual(
+            0,
+            self.suite_tr.run_tests(test_infos, extra_args, mock_reporter))
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/atest/test_runners/test_runner_base.py b/atest/test_runners/test_runner_base.py
new file mode 100644
index 0000000..22994e3
--- /dev/null
+++ b/atest/test_runners/test_runner_base.py
@@ -0,0 +1,205 @@
+# Copyright 2017, 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.
+
+"""
+Base test runner class.
+
+Class that other test runners will instantiate for test runners.
+"""
+
+from __future__ import print_function
+import errno
+import logging
+import signal
+import subprocess
+import tempfile
+import os
+import sys
+
+from collections import namedtuple
+
+# pylint: disable=import-error
+import atest_error
+import atest_utils
+import constants
+
+OLD_OUTPUT_ENV_VAR = 'ATEST_OLD_OUTPUT'
+
+# TestResult contains information of individual tests during a test run.
+TestResult = namedtuple('TestResult', ['runner_name', 'group_name',
+                                       'test_name', 'status', 'details',
+                                       'test_count', 'test_time',
+                                       'runner_total', 'group_total',
+                                       'additional_info', 'test_run_name'])
+ASSUMPTION_FAILED = 'ASSUMPTION_FAILED'
+FAILED_STATUS = 'FAILED'
+PASSED_STATUS = 'PASSED'
+IGNORED_STATUS = 'IGNORED'
+ERROR_STATUS = 'ERROR'
+
+class TestRunnerBase(object):
+    """Base Test Runner class."""
+    NAME = ''
+    EXECUTABLE = ''
+
+    def __init__(self, results_dir, **kwargs):
+        """Init stuff for base class."""
+        self.results_dir = results_dir
+        self.test_log_file = None
+        if not self.NAME:
+            raise atest_error.NoTestRunnerName('Class var NAME is not defined.')
+        if not self.EXECUTABLE:
+            raise atest_error.NoTestRunnerExecutable('Class var EXECUTABLE is '
+                                                     'not defined.')
+        if kwargs:
+            logging.debug('ignoring the following args: %s', kwargs)
+
+    def run(self, cmd, output_to_stdout=False, env_vars=None):
+        """Shell out and execute command.
+
+        Args:
+            cmd: A string of the command to execute.
+            output_to_stdout: A boolean. If False, the raw output of the run
+                              command will not be seen in the terminal. This
+                              is the default behavior, since the test_runner's
+                              run_tests() method should use atest's
+                              result reporter to print the test results.
+
+                              Set to True to see the output of the cmd. This
+                              would be appropriate for verbose runs.
+            env_vars: Environment variables passed to the subprocess.
+        """
+        if not output_to_stdout:
+            self.test_log_file = tempfile.NamedTemporaryFile(mode='w',
+                                                             dir=self.results_dir,
+                                                             delete=True)
+        logging.debug('Executing command: %s', cmd)
+        return subprocess.Popen(cmd, preexec_fn=os.setsid, shell=True,
+                                stderr=subprocess.STDOUT, stdout=self.test_log_file,
+                                env=env_vars)
+
+    # pylint: disable=broad-except
+    def handle_subprocess(self, subproc, func):
+        """Execute the function. Interrupt the subproc when exception occurs.
+
+        Args:
+            subproc: A subprocess to be terminated.
+            func: A function to be run.
+        """
+        try:
+            signal.signal(signal.SIGINT, self._signal_passer(subproc))
+            func()
+        except Exception as error:
+            # exc_info=1 tells logging to log the stacktrace
+            logging.debug('Caught exception:', exc_info=1)
+            # Remember our current exception scope, before new try block
+            # Python3 will make this easier, the error itself stores
+            # the scope via error.__traceback__ and it provides a
+            # "raise from error" pattern.
+            # https://docs.python.org/3.5/reference/simple_stmts.html#raise
+            exc_type, exc_msg, traceback_obj = sys.exc_info()
+            # If atest crashes, try to kill subproc group as well.
+            try:
+                logging.debug('Killing subproc: %s', subproc.pid)
+                os.killpg(os.getpgid(subproc.pid), signal.SIGINT)
+            except OSError:
+                # this wipes our previous stack context, which is why
+                # we have to save it above.
+                logging.debug('Subproc already terminated, skipping')
+            finally:
+                if self.test_log_file:
+                    with open(self.test_log_file.name, 'r') as f:
+                        intro_msg = "Unexpected Issue. Raw Output:"
+                        print(atest_utils.colorize(intro_msg, constants.RED))
+                        print(f.read())
+                # Ignore socket.recv() raising due to ctrl-c
+                if not error.args or error.args[0] != errno.EINTR:
+                    raise exc_type, exc_msg, traceback_obj
+
+    def wait_for_subprocess(self, proc):
+        """Check the process status. Interrupt the TF subporcess if user
+        hits Ctrl-C.
+
+        Args:
+            proc: The tradefed subprocess.
+
+        Returns:
+            Return code of the subprocess for running tests.
+        """
+        try:
+            logging.debug('Runner Name: %s, Process ID: %s', self.NAME, proc.pid)
+            signal.signal(signal.SIGINT, self._signal_passer(proc))
+            proc.wait()
+            return proc.returncode
+        except:
+            # If atest crashes, kill TF subproc group as well.
+            os.killpg(os.getpgid(proc.pid), signal.SIGINT)
+            raise
+
+    def _signal_passer(self, proc):
+        """Return the signal_handler func bound to proc.
+
+        Args:
+            proc: The tradefed subprocess.
+
+        Returns:
+            signal_handler function.
+        """
+        def signal_handler(_signal_number, _frame):
+            """Pass SIGINT to proc.
+
+            If user hits ctrl-c during atest run, the TradeFed subprocess
+            won't stop unless we also send it a SIGINT. The TradeFed process
+            is started in a process group, so this SIGINT is sufficient to
+            kill all the child processes TradeFed spawns as well.
+            """
+            logging.info('Ctrl-C received. Killing subprocess group')
+            os.killpg(os.getpgid(proc.pid), signal.SIGINT)
+        return signal_handler
+
+    def run_tests(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos.
+
+        Should contain code for kicking off the test runs using
+        test_runner_base.run(). Results should be processed and printed
+        via the reporter passed in.
+
+        Args:
+            test_infos: List of TestInfo.
+            extra_args: Dict of extra args to add to test run.
+            reporter: An instance of result_report.ResultReporter.
+        """
+        raise NotImplementedError
+
+    def host_env_check(self):
+        """Checks that host env has met requirements."""
+        raise NotImplementedError
+
+    def get_test_runner_build_reqs(self):
+        """Returns a list of build targets required by the test runner."""
+        raise NotImplementedError
+
+    def generate_run_commands(self, test_infos, extra_args, port=None):
+        """Generate a list of run commands from TestInfos.
+
+        Args:
+            test_infos: A set of TestInfo instances.
+            extra_args: A Dict of extra args to append.
+            port: Optional. An int of the port number to send events to.
+                  Subprocess reporter in TF won't try to connect if it's None.
+
+        Returns:
+            A list of run commands to run the tests.
+        """
+        raise NotImplementedError
diff --git a/atest/test_runners/vts_tf_test_runner.py b/atest/test_runners/vts_tf_test_runner.py
new file mode 100644
index 0000000..9e6801b
--- /dev/null
+++ b/atest/test_runners/vts_tf_test_runner.py
@@ -0,0 +1,129 @@
+# Copyright 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.
+
+"""
+VTS Tradefed test runner class.
+"""
+
+import copy
+import logging
+
+# pylint: disable=import-error
+from test_runners import atest_tf_test_runner
+import atest_utils
+import constants
+
+
+class VtsTradefedTestRunner(atest_tf_test_runner.AtestTradefedTestRunner):
+    """TradeFed Test Runner class."""
+    NAME = 'VtsTradefedTestRunner'
+    EXECUTABLE = 'vts-tradefed'
+    _RUN_CMD = ('{exe} run commandAndExit {plan} -m {test} {args}')
+    _BUILD_REQ = {'vts-tradefed-standalone'}
+    _DEFAULT_ARGS = ['--skip-all-system-status-check',
+                     '--skip-preconditions',
+                     '--primary-abi-only']
+
+    def __init__(self, results_dir, **kwargs):
+        """Init stuff for vts tradefed runner class."""
+        super(VtsTradefedTestRunner, self).__init__(results_dir, **kwargs)
+        self.run_cmd_dict = {'exe': self.EXECUTABLE,
+                             'test': '',
+                             'args': ''}
+
+    def get_test_runner_build_reqs(self):
+        """Return the build requirements.
+
+        Returns:
+            Set of build targets.
+        """
+        build_req = self._BUILD_REQ
+        build_req |= super(VtsTradefedTestRunner,
+                           self).get_test_runner_build_reqs()
+        return build_req
+
+    def run_tests(self, test_infos, extra_args, reporter):
+        """Run the list of test_infos.
+
+        Args:
+            test_infos: List of TestInfo.
+            extra_args: Dict of extra args to add to test run.
+            reporter: An instance of result_report.ResultReporter.
+
+        Returns:
+            Return code of the process for running tests.
+        """
+        ret_code = constants.EXIT_CODE_SUCCESS
+        reporter.register_unsupported_runner(self.NAME)
+        run_cmds = self.generate_run_commands(test_infos, extra_args)
+        for run_cmd in run_cmds:
+            proc = super(VtsTradefedTestRunner, self).run(run_cmd,
+                                                          output_to_stdout=True)
+            ret_code |= self.wait_for_subprocess(proc)
+        return ret_code
+
+    def _parse_extra_args(self, extra_args):
+        """Convert the extra args into something vts-tf can understand.
+
+        We want to transform the top-level args from atest into specific args
+        that vts-tradefed supports. The only arg we take as is is EXTRA_ARG
+        since that is what the user intentionally wants to pass to the test
+        runner.
+
+        Args:
+            extra_args: Dict of args
+
+        Returns:
+            List of args to append.
+        """
+        args_to_append = []
+        args_not_supported = []
+        for arg in extra_args:
+            if constants.SERIAL == arg:
+                args_to_append.append('--serial')
+                args_to_append.append(extra_args[arg])
+                continue
+            if constants.CUSTOM_ARGS == arg:
+                args_to_append.extend(extra_args[arg])
+                continue
+            if constants.DRY_RUN == arg:
+                continue
+            args_not_supported.append(arg)
+        if args_not_supported:
+            logging.info('%s does not support the following args: %s',
+                         self.EXECUTABLE, args_not_supported)
+        return args_to_append
+
+    # pylint: disable=arguments-differ
+    def generate_run_commands(self, test_infos, extra_args):
+        """Generate a list of run commands from TestInfos.
+
+        Args:
+            test_infos: List of TestInfo tests to run.
+            extra_args: Dict of extra args to add to test run.
+
+        Returns:
+            A List of strings that contains the vts-tradefed run command.
+        """
+        cmds = []
+        args = self._DEFAULT_ARGS
+        args.extend(self._parse_extra_args(extra_args))
+        args.extend(atest_utils.get_result_server_args())
+        for test_info in test_infos:
+            cmd_dict = copy.deepcopy(self.run_cmd_dict)
+            cmd_dict['plan'] = constants.VTS_STAGING_PLAN
+            cmd_dict['test'] = test_info.test_name
+            cmd_dict['args'] = ' '.join(args)
+            cmds.append(self._RUN_CMD.format(**cmd_dict))
+        return cmds
diff --git a/atest/test_runners/vts_tf_test_runner_unittest.py b/atest/test_runners/vts_tf_test_runner_unittest.py
new file mode 100755
index 0000000..7e8b408
--- /dev/null
+++ b/atest/test_runners/vts_tf_test_runner_unittest.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+#
+# Copyright 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.
+"""Unittests for vts_tf_test_runner."""
+
+import unittest
+import mock
+
+# pylint: disable=import-error
+from test_runners import vts_tf_test_runner
+
+# pylint: disable=protected-access
+class VtsTradefedTestRunnerUnittests(unittest.TestCase):
+    """Unit tests for vts_tf_test_runner.py"""
+
+    def setUp(self):
+        self.vts_tr = vts_tf_test_runner.VtsTradefedTestRunner(results_dir='')
+
+    def tearDown(self):
+        mock.patch.stopall()
+
+    @mock.patch('subprocess.Popen')
+    @mock.patch.object(vts_tf_test_runner.VtsTradefedTestRunner, 'run')
+    @mock.patch.object(vts_tf_test_runner.VtsTradefedTestRunner,
+                       'generate_run_commands')
+    def test_run_tests(self, _mock_gen_cmd, _mock_run, _mock_popen):
+        """Test run_tests method."""
+        test_infos = []
+        extra_args = []
+        mock_reporter = mock.Mock()
+        _mock_gen_cmd.return_value = ["cmd1", "cmd2"]
+        # Test Build Pass
+        _mock_popen.return_value.returncode = 0
+        self.assertEqual(
+            0,
+            self.vts_tr.run_tests(test_infos, extra_args, mock_reporter))
+
+        # Test Build Pass
+        _mock_popen.return_value.returncode = 1
+        self.assertNotEqual(
+            0,
+            self.vts_tr.run_tests(test_infos, extra_args, mock_reporter))
+
+
+if __name__ == '__main__':
+    unittest.main()
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_tools.py b/atest/tools/atest_tools.py
new file mode 100755
index 0000000..8a59e07
--- /dev/null
+++ b/atest/tools/atest_tools.py
@@ -0,0 +1,354 @@
+#!/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 tool functions.
+"""
+
+from __future__ import print_function
+
+import logging
+import os
+import pickle
+import shutil
+import subprocess
+import sys
+
+import constants
+import module_info
+
+from metrics import metrics_utils
+
+MAC_UPDB_SRC = os.path.join(os.path.dirname(__file__), 'updatedb_darwin.sh')
+MAC_UPDB_DST = os.path.join(os.getenv(constants.ANDROID_HOST_OUT, ''), 'bin')
+UPDATEDB = 'updatedb'
+LOCATE = 'locate'
+SEARCH_TOP = os.getenv(constants.ANDROID_BUILD_TOP, '')
+MACOSX = 'Darwin'
+OSNAME = os.uname()[0]
+INDEXES = (constants.CLASS_INDEX, constants.QCLASS_INDEX,
+           constants.PACKAGE_INDEX, constants.CC_CLASS_INDEX)
+
+# The list was generated by command:
+# find `gettop` -type d -wholename `gettop`/out -prune  -o -type d -name '.*'
+# -print | awk -F/ '{{print $NF}}'| sort -u
+PRUNENAMES = ['.abc', '.appveyor', '.azure-pipelines',
+              '.bazelci', '.buildscript', '.ci', '.circleci', '.conan',
+              '.externalToolBuilders',
+              '.git', '.github', '.github-ci', '.google', '.gradle',
+              '.idea', '.intermediates',
+              '.jenkins',
+              '.kokoro',
+              '.libs_cffi_backend',
+              '.mvn',
+              '.prebuilt_info', '.private', '__pycache__',
+              '.repo',
+              '.semaphore', '.settings', '.static', '.svn',
+              '.test', '.travis', '.tx',
+              '.vscode']
+# Running locate + grep consumes tremendous amount of time in MacOS. Running it
+# with a physical script file can increase the performance.
+TMPRUN = '/tmp/._'
+
+def _mkdir_when_inexists(dirname):
+    if not os.path.isdir(dirname):
+        os.makedirs(dirname)
+
+def _install_updatedb():
+    """Install a customized updatedb for MacOS and ensure it is executable."""
+    _mkdir_when_inexists(MAC_UPDB_DST)
+    _mkdir_when_inexists(constants.INDEX_DIR)
+    if OSNAME == MACOSX:
+        shutil.copy2(MAC_UPDB_SRC, os.path.join(MAC_UPDB_DST, UPDATEDB))
+        os.chmod(os.path.join(MAC_UPDB_DST, UPDATEDB), 0755)
+
+def _delete_indexes():
+    """Delete all available index files."""
+    for index in INDEXES:
+        if os.path.isfile(index):
+            os.remove(index)
+
+def has_command(cmd):
+    """Detect if the command is available in PATH.
+
+    shutil.which('cmd') is only valid in Py3 so we need to customise it.
+
+    Args:
+        cmd: A string of the tested command.
+
+    Returns:
+        True if found, False otherwise."""
+    paths = os.getenv('PATH', '').split(':')
+    for path in paths:
+        if os.path.isfile(os.path.join(path, cmd)):
+            return True
+    return False
+
+def run_updatedb(search_root=SEARCH_TOP, output_cache=constants.LOCATE_CACHE,
+                 **kwargs):
+    """Run updatedb and generate cache in $ANDROID_HOST_OUT/indexes/mlocate.db
+
+    Args:
+        search_root: The path of the search root(-U).
+        output_cache: The filename of the updatedb cache(-o).
+        kwargs: (optional)
+            prunepaths: A list of paths unwanted to be searched(-e).
+            prunenames: A list of dirname that won't be cached(-n).
+    """
+    prunenames = kwargs.pop('prunenames', ' '.join(PRUNENAMES))
+    prunepaths = kwargs.pop('prunepaths', os.path.join(search_root, 'out'))
+    if kwargs:
+        raise TypeError('Unexpected **kwargs: %r' % kwargs)
+    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)
+
+    if not has_command(UPDATEDB):
+        return
+    logging.debug('Running updatedb... ')
+    try:
+        full_env_vars = os.environ.copy()
+        logging.debug('Executing: %s', updatedb_cmd)
+        subprocess.check_call(updatedb_cmd, env=full_env_vars)
+    except (KeyboardInterrupt, SystemExit):
+        logging.error('Process interrupted or failure.')
+
+def _dump_index(dump_file, output, output_re, key, value):
+    """Dump indexed data with pickle.
+
+    Args:
+        dump_file: A string of absolute path of the index file.
+        output: A string generated by locate and grep.
+        output_re: An regex which is used for grouping patterns.
+        key: A string for dictionary key, e.g. classname, package, cc_class, etc.
+        value: A set of path.
+
+    The data structure will be like:
+    {
+      'Foo': {'/path/to/Foo.java', '/path2/to/Foo.kt'},
+      'Boo': {'/path3/to/Boo.java'}
+    }
+    """
+    _dict = {}
+    with open(dump_file, 'wb') as cache_file:
+        for entry in output.splitlines():
+            match = output_re.match(entry)
+            if match:
+                _dict.setdefault(match.group(key), set()).add(match.group(value))
+        try:
+            pickle.dump(_dict, cache_file, protocol=2)
+        except IOError:
+            os.remove(dump_file)
+            logging.error('Failed in dumping %s', dump_file)
+
+def _get_cc_result(locatedb=None):
+    """Search all testable cc/cpp and grep TEST(), TEST_F() or TEST_P().
+
+    Return:
+        A string object generated by subprocess.
+    """
+    if not locatedb:
+        locatedb = constants.LOCATE_CACHE
+    cc_grep_re = r'^\s*TEST(_P|_F)?\s*\([[:alnum:]]+,'
+    if OSNAME == MACOSX:
+        find_cmd = (r"locate -d {0} '*.cpp' '*.cc' | grep -i test "
+                    "| xargs egrep -sH '{1}' || true")
+    else:
+        find_cmd = (r"locate -d {0} / | egrep -i '/*.test.*\.(cc|cpp)$' "
+                    "| xargs egrep -sH '{1}' || true")
+    find_cc_cmd = find_cmd.format(locatedb, cc_grep_re)
+    logging.debug('Probing CC classes:\n %s', find_cc_cmd)
+    return subprocess.check_output('echo \"%s\" > %s; sh %s'
+                                   % (find_cc_cmd, TMPRUN, TMPRUN), shell=True)
+
+def _get_java_result(locatedb=None):
+    """Search all testable java/kt and grep package.
+
+    Return:
+        A string object generated by subprocess.
+    """
+    if not locatedb:
+        locatedb = constants.LOCATE_CACHE
+    package_grep_re = r'^\s*package\s+[a-z][[:alnum:]]+[^{]'
+    if OSNAME == MACOSX:
+        find_cmd = r"locate -d%s '*.java' '*.kt'|grep -i test" % locatedb
+    else:
+        find_cmd = r"locate -d%s / | egrep -i '/*.test.*\.(java|kt)$'" % locatedb
+    find_java_cmd = find_cmd + '| xargs egrep -sH \'%s\' || true' % package_grep_re
+    logging.debug('Probing Java classes:\n %s', find_java_cmd)
+    return subprocess.check_output('echo \"%s\" > %s; sh %s'
+                                   % (find_java_cmd, TMPRUN, TMPRUN), shell=True)
+
+def _index_testable_modules(index):
+    """Dump testable modules read by tab completion.
+
+    Args:
+        index: A string path of the index file.
+    """
+    logging.debug('indexing testable modules.')
+    testable_modules = module_info.ModuleInfo().get_testable_modules()
+    with open(index, 'wb') as cache:
+        try:
+            pickle.dump(testable_modules, cache, protocol=2)
+        except IOError:
+            os.remove(cache)
+            logging.error('Failed in dumping %s', cache)
+
+def _index_cc_classes(output, index):
+    """Index Java classes.
+
+    The data structure is like:
+    {
+      'FooTestCase': {'/path1/to/the/FooTestCase.java',
+                      '/path2/to/the/FooTestCase.kt'}
+    }
+
+    Args:
+        output: A string object generated by _get_cc_result().
+        index: A string path of the index file.
+    """
+    logging.debug('indexing CC classes.')
+    _dump_index(dump_file=index, output=output,
+                output_re=constants.CC_OUTPUT_RE,
+                key='test_name', value='file_path')
+
+def _index_java_classes(output, index):
+    """Index Java classes.
+    The data structure is like:
+    {
+        'FooTestCase': {'/path1/to/the/FooTestCase.java',
+                        '/path2/to/the/FooTestCase.kt'}
+    }
+
+    Args:
+        output: A string object generated by _get_java_result().
+        index: A string path of the index file.
+    """
+    logging.debug('indexing Java classes.')
+    _dump_index(dump_file=index, output=output,
+                output_re=constants.CLASS_OUTPUT_RE,
+                key='class', value='java_path')
+
+def _index_packages(output, index):
+    """Index Java packages.
+    The data structure is like:
+    {
+        'a.b.c.d': {'/path1/to/a/b/c/d/',
+                    '/path2/to/a/b/c/d/'
+    }
+
+    Args:
+        output: A string object generated by _get_java_result().
+        index: A string path of the index file.
+    """
+    logging.debug('indexing packages.')
+    _dump_index(dump_file=index,
+                output=output, output_re=constants.PACKAGE_OUTPUT_RE,
+                key='package', value='java_dir')
+
+def _index_qualified_classes(output, index):
+    """Index Fully Qualified Java Classes(FQCN).
+    The data structure is like:
+    {
+        'a.b.c.d.FooTestCase': {'/path1/to/a/b/c/d/FooTestCase.java',
+                                '/path2/to/a/b/c/d/FooTestCase.kt'}
+    }
+
+    Args:
+        output: A string object generated by _get_java_result().
+        index: A string path of the index file.
+    """
+    logging.debug('indexing qualified classes.')
+    _dict = {}
+    with open(index, 'wb') as cache_file:
+        for entry in output.split('\n'):
+            match = constants.QCLASS_OUTPUT_RE.match(entry)
+            if match:
+                fqcn = match.group('package') + '.' + match.group('class')
+                _dict.setdefault(fqcn, set()).add(match.group('java_path'))
+        try:
+            pickle.dump(_dict, cache_file, protocol=2)
+        except (KeyboardInterrupt, SystemExit):
+            logging.error('Process interrupted or failure.')
+            os.remove(index)
+        except IOError:
+            logging.error('Failed in dumping %s', index)
+
+def index_targets(output_cache=constants.LOCATE_CACHE, **kwargs):
+    """The entrypoint of indexing targets.
+
+    Utilise mlocate database to index reference types of CLASS, CC_CLASS,
+    PACKAGE and QUALIFIED_CLASS. Testable module for tab completion is also
+    generated in this method.
+
+    Args:
+        output_cache: A file path of the updatedb cache(e.g. /path/to/mlocate.db).
+        kwargs: (optional)
+            class_index: A path string of the Java class index.
+            qclass_index: A path string of the qualified class index.
+            package_index: A path string of the package index.
+            cc_class_index: A path string of the CC class index.
+            module_index: A path string of the testable module index.
+            integration_index: A path string of the integration index.
+    """
+    class_index = kwargs.pop('class_index', constants.CLASS_INDEX)
+    qclass_index = kwargs.pop('qclass_index', constants.QCLASS_INDEX)
+    package_index = kwargs.pop('package_index', constants.PACKAGE_INDEX)
+    cc_class_index = kwargs.pop('cc_class_index', constants.CC_CLASS_INDEX)
+    module_index = kwargs.pop('module_index', constants.MODULE_INDEX)
+    # Uncomment below if we decide to support INTEGRATION.
+    #integration_index = kwargs.pop('integration_index', constants.INT_INDEX)
+    if kwargs:
+        raise TypeError('Unexpected **kwargs: %r' % kwargs)
+
+    try:
+        # Step 0: generate mlocate database prior to indexing targets.
+        run_updatedb(SEARCH_TOP, constants.LOCATE_CACHE)
+        if not has_command(LOCATE):
+            return
+        # Step 1: generate output string for indexing targets.
+        logging.debug('Indexing targets... ')
+        cc_result = _get_cc_result(output_cache)
+        java_result = _get_java_result(output_cache)
+        # Step 2: index Java and CC classes.
+        _index_cc_classes(cc_result, cc_class_index)
+        _index_java_classes(java_result, class_index)
+        _index_qualified_classes(java_result, qclass_index)
+        _index_packages(java_result, package_index)
+        _index_testable_modules(module_index)
+        # Step 3: Clean up the temp script.
+        if os.path.isfile(TMPRUN):
+            os.remove(TMPRUN)
+    # Delete indexes when mlocate.db is locked() or other CalledProcessError.
+    # (b/141588997)
+    except subprocess.CalledProcessError as err:
+        logging.error('Executing %s error.', UPDATEDB)
+        metrics_utils.handle_exc_and_send_exit_event(
+            constants.MLOCATEDB_LOCKED)
+        if err.output:
+            logging.error(err.output)
+        _delete_indexes()
+
+
+if __name__ == '__main__':
+    if not os.getenv(constants.ANDROID_HOST_OUT, ''):
+        sys.exit()
+    index_targets()
diff --git a/atest/tools/atest_tools_unittest.py b/atest/tools/atest_tools_unittest.py
new file mode 100755
index 0000000..f53548f
--- /dev/null
+++ b/atest/tools/atest_tools_unittest.py
@@ -0,0 +1,99 @@
+#!/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_tools."""
+
+import os
+import pickle
+import platform
+import subprocess
+import sys
+import unittest
+import mock
+
+import atest_tools
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+# pylint: disable=wrong-import-position
+import unittest_constants as uc
+
+SEARCH_ROOT = uc.TEST_DATA_DIR
+PRUNEPATH = uc.TEST_CONFIG_DATA_DIR
+
+
+class AtestToolsUnittests(unittest.TestCase):
+    """"Unittest Class for atest_tools.py."""
+
+    @mock.patch('module_info.ModuleInfo.get_testable_modules')
+    @mock.patch('module_info.ModuleInfo.__init__')
+    def test_index_targets(self, mock_mod_info, mock_testable_mod):
+        """Test method index_targets."""
+        mock_mod_info.return_value = None
+        mock_testable_mod.return_value = set()
+        if atest_tools.has_command('updatedb'):
+            atest_tools.run_updatedb(SEARCH_ROOT, uc.LOCATE_CACHE,
+                                     prunepaths=PRUNEPATH)
+            # test_config/ is excluded so that a.xml won't be found.
+            locate_cmd1 = ['locate', '-d', uc.LOCATE_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', uc.LOCATE_CACHE, 'module-info.json']
+            self.assertEqual(subprocess.call(locate_cmd2), 0)
+        else:
+            self.assertEqual(atest_tools.has_command('updatedb'), False)
+
+        if atest_tools.has_command('locate'):
+            atest_tools.index_targets(uc.LOCATE_CACHE,
+                                      class_index=uc.CLASS_INDEX,
+                                      qclass_index=uc.QCLASS_INDEX,
+                                      cc_class_index=uc.CC_CLASS_INDEX,
+                                      package_index=uc.PACKAGE_INDEX,
+                                      module_index=uc.MODULE_INDEX)
+            _dict = {}
+            # Test finding a Java class
+            with open(uc.CLASS_INDEX, 'rb') as _cache:
+                _dict = pickle.load(_cache)
+            self.assertIsNotNone(_dict.get('PathTesting'))
+            # Test finding a CC class
+            with open(uc.CC_CLASS_INDEX, 'rb') as _cache:
+                _dict = pickle.load(_cache)
+            self.assertIsNotNone(_dict.get('HelloWorldTest'))
+            # Test finding a package
+            with open(uc.PACKAGE_INDEX, 'rb') as _cache:
+                _dict = pickle.load(_cache)
+            self.assertIsNotNone(_dict.get('android.jank.cts.ui'))
+            # Test finding a fully qualified class name
+            with open(uc.QCLASS_INDEX, 'rb') as _cache:
+                _dict = pickle.load(_cache)
+            self.assertIsNotNone(_dict.get('android.jank.cts.ui.PathTesting'))
+            # Clean up.
+            targets_to_delete = (uc.LOCATE_CACHE,
+                                 uc.CLASS_INDEX,
+                                 uc.QCLASS_INDEX,
+                                 uc.CC_CLASS_INDEX,
+                                 uc.PACKAGE_INDEX,
+                                 uc.MODULE_INDEX)
+            for idx in targets_to_delete:
+                os.remove(idx)
+        else:
+            self.assertEqual(atest_tools.has_command('locate'), False)
+
+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
new file mode 100644
index 0000000..c757936
--- /dev/null
+++ b/atest/unittest_constants.py
@@ -0,0 +1,247 @@
+# Copyright 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.
+
+"""
+Unittest constants.
+
+Unittest constants get their own file since they're used purely for testing and
+should not be combined with constants_defaults as part of normal atest
+operation. These constants are used commonly as test data so when updating a
+constant, do so with care and run all unittests to make sure nothing breaks.
+"""
+
+import os
+
+import constants
+from test_finders import test_info
+from test_runners import atest_tf_test_runner as atf_tr
+
+ROOT = '/'
+MODULE_DIR = 'foo/bar/jank'
+MODULE2_DIR = 'foo/bar/hello'
+MODULE_NAME = 'CtsJankDeviceTestCases'
+TYPO_MODULE_NAME = 'CtsJankDeviceTestCase'
+MODULE2_NAME = 'HelloWorldTests'
+CLASS_NAME = 'CtsDeviceJankUi'
+FULL_CLASS_NAME = 'android.jank.cts.ui.CtsDeviceJankUi'
+PACKAGE = 'android.jank.cts.ui'
+FIND_ONE = ROOT + 'foo/bar/jank/src/android/jank/cts/ui/CtsDeviceJankUi.java\n'
+FIND_TWO = ROOT + 'other/dir/test.java\n' + FIND_ONE
+FIND_PKG = ROOT + 'foo/bar/jank/src/android/jank/cts/ui\n'
+INT_NAME = 'example/reboot'
+GTF_INT_NAME = 'some/gtf_int_test'
+TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'unittest_data')
+TEST_CONFIG_DATA_DIR = os.path.join(TEST_DATA_DIR, 'test_config')
+
+INT_DIR = 'tf/contrib/res/config'
+GTF_INT_DIR = 'gtf/core/res/config'
+
+CONFIG_FILE = os.path.join(MODULE_DIR, constants.MODULE_CONFIG)
+CONFIG2_FILE = os.path.join(MODULE2_DIR, constants.MODULE_CONFIG)
+JSON_FILE = 'module-info.json'
+MODULE_INFO_TARGET = '/out/%s' % JSON_FILE
+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])}
+PACKAGE_FILTER = test_info.TestFilter(PACKAGE, frozenset())
+PACKAGE_DATA = {constants.TI_REL_CONFIG: CONFIG_FILE,
+                constants.TI_FILTER: frozenset([PACKAGE_FILTER])}
+TEST_DATA_CONFIG = os.path.relpath(os.path.join(TEST_DATA_DIR,
+                                                constants.MODULE_CONFIG), ROOT)
+PATH_DATA = {
+    constants.TI_REL_CONFIG: TEST_DATA_CONFIG,
+    constants.TI_FILTER: frozenset([PACKAGE_FILTER])}
+EMPTY_PATH_DATA = {
+    constants.TI_REL_CONFIG: TEST_DATA_CONFIG,
+    constants.TI_FILTER: frozenset()}
+
+CLASS_BUILD_TARGETS = {'class-specific-target'}
+CLASS_INFO = test_info.TestInfo(MODULE_NAME,
+                                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,
+                                  PACKAGE_DATA)
+PATH_INFO = test_info.TestInfo(MODULE_NAME,
+                               atf_tr.AtestTradefedTestRunner.NAME,
+                               MODULE_BUILD_TARGETS,
+                               PATH_DATA)
+EMPTY_PATH_INFO = test_info.TestInfo(MODULE_NAME,
+                                     atf_tr.AtestTradefedTestRunner.NAME,
+                                     MODULE_BUILD_TARGETS,
+                                     EMPTY_PATH_DATA)
+MODULE_CLASS_COMBINED_BUILD_TARGETS = MODULE_BUILD_TARGETS | CLASS_BUILD_TARGETS
+INT_CONFIG = os.path.join(INT_DIR, INT_NAME + '.xml')
+GTF_INT_CONFIG = os.path.join(GTF_INT_DIR, GTF_INT_NAME + '.xml')
+METHOD_NAME = 'method1'
+METHOD_FILTER = test_info.TestFilter(FULL_CLASS_NAME, frozenset([METHOD_NAME]))
+METHOD_INFO = test_info.TestInfo(
+    MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    MODULE_BUILD_TARGETS,
+    data={constants.TI_FILTER: frozenset([METHOD_FILTER]),
+          constants.TI_REL_CONFIG: CONFIG_FILE})
+METHOD2_NAME = 'method2'
+FLAT_METHOD_FILTER = test_info.TestFilter(
+    FULL_CLASS_NAME, frozenset([METHOD_NAME, METHOD2_NAME]))
+INT_INFO = test_info.TestInfo(INT_NAME,
+                              atf_tr.AtestTradefedTestRunner.NAME,
+                              set(),
+                              data={constants.TI_REL_CONFIG: INT_CONFIG,
+                                    constants.TI_FILTER: frozenset()})
+GTF_INT_INFO = test_info.TestInfo(
+    GTF_INT_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    set(),
+    data={constants.TI_FILTER: frozenset(),
+          constants.TI_REL_CONFIG: GTF_INT_CONFIG})
+
+# Sample test configurations in TEST_MAPPING file.
+TEST_MAPPING_TEST = {'name': MODULE_NAME, 'host': True}
+TEST_MAPPING_TEST_WITH_OPTION = {
+    'name': CLASS_NAME,
+    'options': [
+        {
+            'arg1': 'val1'
+        },
+        {
+            'arg2': ''
+        }
+    ]
+}
+TEST_MAPPING_TEST_WITH_OPTION_STR = '%s (arg1: val1, arg2:)' % CLASS_NAME
+TEST_MAPPING_TEST_WITH_BAD_OPTION = {
+    'name': CLASS_NAME,
+    'options': [
+        {
+            'arg1': 'val1',
+            'arg2': ''
+        }
+    ]
+}
+TEST_MAPPING_TEST_WITH_BAD_HOST_VALUE = {
+    'name': CLASS_NAME,
+    'host': 'true'
+}
+# Constrants of cc test unittest
+FIND_CC_ONE = ROOT + 'foo/bt/hci/test/pf_test.cc\n'
+CC_MODULE_NAME = 'net_test_hci'
+CC_CLASS_NAME = 'PFTest'
+CC_MODULE_DIR = 'system/bt/hci'
+CC_CLASS_FILTER = test_info.TestFilter(CC_CLASS_NAME+".*", frozenset())
+CC_CONFIG_FILE = os.path.join(CC_MODULE_DIR, constants.MODULE_CONFIG)
+CC_MODULE_CLASS_DATA = {constants.TI_REL_CONFIG: CC_CONFIG_FILE,
+                        constants.TI_FILTER: frozenset([CC_CLASS_FILTER])}
+CC_MODULE_CLASS_INFO = test_info.TestInfo(CC_MODULE_NAME,
+                                          atf_tr.AtestTradefedTestRunner.NAME,
+                                          CLASS_BUILD_TARGETS, CC_MODULE_CLASS_DATA)
+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_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,
+                 constants.TI_FILTER: frozenset([CC_CLASS_FILTER])}
+CC_CLASS_INFO = test_info.TestInfo(CC_MODULE_NAME,
+                                   atf_tr.AtestTradefedTestRunner.NAME,
+                                   CLASS_BUILD_TARGETS, CC_CLASS_DATA)
+CC_METHOD_NAME = 'test1'
+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_CLASS_NAME+"."+CC_METHOD2_NAME,
+                                         frozenset())
+CC_METHOD_INFO = test_info.TestInfo(
+    CC_MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    MODULE_BUILD_TARGETS,
+    data={constants.TI_REL_CONFIG: CC_CONFIG_FILE,
+          constants.TI_FILTER: frozenset([CC_METHOD_FILTER])})
+CC_METHOD2_INFO = test_info.TestInfo(
+    CC_MODULE_NAME,
+    atf_tr.AtestTradefedTestRunner.NAME,
+    MODULE_BUILD_TARGETS,
+    data={constants.TI_REL_CONFIG: CC_CONFIG_FILE,
+          constants.TI_FILTER: frozenset([CC_METHOD2_FILTER])})
+CC_PATH_DATA = {
+    constants.TI_REL_CONFIG: TEST_DATA_CONFIG,
+    constants.TI_FILTER: frozenset()}
+CC_PATH_INFO = test_info.TestInfo(CC_MODULE_NAME,
+                                  atf_tr.AtestTradefedTestRunner.NAME,
+                                  MODULE_BUILD_TARGETS,
+                                  CC_PATH_DATA)
+CC_PATH_DATA2 = {constants.TI_REL_CONFIG: CC_CONFIG_FILE,
+                 constants.TI_FILTER: frozenset()}
+CC_PATH_INFO2 = test_info.TestInfo(CC_MODULE_NAME,
+                                   atf_tr.AtestTradefedTestRunner.NAME,
+                                   CLASS_BUILD_TARGETS, CC_PATH_DATA2)
+CTS_INT_DIR = 'test/suite_harness/tools/cts-tradefed/res/config'
+# Constrants of java, kt, cc, cpp test_find_class_file() unittest
+FIND_PATH_TESTCASE_JAVA = 'hello_world_test'
+FIND_PATH_FILENAME_CC = 'hello_world_test'
+FIND_PATH_TESTCASE_CC = 'HelloWorldTest'
+FIND_PATH_FOLDER = 'class_file_path_testing'
+FIND_PATH = os.path.join(TEST_DATA_DIR, FIND_PATH_FOLDER)
+
+DEFAULT_INSTALL_PATH = ['/path/to/install']
+# Module names
+MOD1 = 'mod1'
+MOD2 = 'mod2'
+MOD3 = 'mod3'
+FUZZY_MOD1 = 'Mod1'
+FUZZY_MOD2 = 'nod2'
+FUZZY_MOD3 = 'mod3mod3'
+
+LOCATE_CACHE = '/tmp/mcloate.db'
+CLASS_INDEX = '/tmp/classes.idx'
+QCLASS_INDEX = '/tmp/fqcn.idx'
+CC_CLASS_INDEX = '/tmp/cc_classes.idx'
+PACKAGE_INDEX = '/tmp/packages.idx'
+MODULE_INDEX = '/tmp/modules.idx'
diff --git a/atest/unittest_data/AndroidTest.xml b/atest/unittest_data/AndroidTest.xml
new file mode 100644
index 0000000..431eafc
--- /dev/null
+++ b/atest/unittest_data/AndroidTest.xml
@@ -0,0 +1,18 @@
+<configuration description="Config for CTS Jank test cases">
+  <option name="test-suite-tag" value="cts" />
+  <option name="not-shardable" value="true" />
+  <option name="config-descriptor:metadata" key="component" value="graphics" />
+  <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+    <option name="cleanup-apks" value="true" />
+    <option name="test-file-name" value="CtsJankDeviceTestCases.apk" />
+    <option name="test-file-name" value="is_not_module.apk" />
+    <option name="push" value="GtsEmptyTestApp.apk->/data/local/tmp/gts/packageinstaller/GtsEmptyTestApp.apk" />
+  </target_preparer>
+  <include name="CtsUiDeviceTestCases"/>
+  <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+    <option name="package" value="android.jank.cts" />
+    <option name="runtime-hint" value="11m20s" />
+  </test>
+  <option name="perf_arg" value="perf-setup.sh" />
+  <test class="com.android.compatibility.class.for.test" />
+</configuration>
diff --git a/atest/unittest_data/CtsUiDeviceTestCases.xml b/atest/unittest_data/CtsUiDeviceTestCases.xml
new file mode 100644
index 0000000..2dd30f9
--- /dev/null
+++ b/atest/unittest_data/CtsUiDeviceTestCases.xml
@@ -0,0 +1,3 @@
+<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+  <option name="test-file-name" value="CtsUiDeviceTestCases.apk" />
+</target_preparer>
diff --git a/atest/unittest_data/VtsAndroidTest.xml b/atest/unittest_data/VtsAndroidTest.xml
new file mode 100644
index 0000000..35c2f4b
--- /dev/null
+++ b/atest/unittest_data/VtsAndroidTest.xml
@@ -0,0 +1,30 @@
+<configuration description="Config for VTS target parsing">
+    <option name="config-descriptor:metadata" key="plan" value="vts-treble" />
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.VtsFilePusher">
+        <option name="abort-on-push-failure" value="false"/>
+        <option name="push-group" value="push_file1.push"/>
+        <option name="push" value="DATA/lib/libhidl-gen-hash.so->/data/local/tmp/32/libhidl-gen-hash.so"/>
+        <option name="push" value="DATA/lib64/libhidl-gen-hash.so->/data/local/tmp/64/libhidl-gen-hash.so"/>
+        <option name="push" value="hal-hidl-hash/frameworks/hardware/interfaces/current.txt->/data/local/tmp/frameworks/hardware/interfaces/current.txt"/>
+        <option name="push" value="hal-hidl-hash/hardware/interfaces/current.txt->/data/local/tmp/hardware/interfaces/current.txt"/>
+        <option name="push" value="hal-hidl-hash/system/hardware/interfaces/current.txt->/data/local/tmp/system/hardware/interfaces/current.txt"/>
+        <option name="push" value="hal-hidl-hash/system/libhidl/transport/current.txt->/data/local/tmp/system/libhidl/transport/current.txt"/>
+    </target_preparer>
+    <multi_target_preparer class="com.android.tradefed.targetprep.VtsPythonVirtualenvPreparer" />
+    <test class="com.android.tradefed.testtype.VtsMultiDeviceTest">
+        <option name="test-module-name" value="VtsTestName"/>
+        <option name="binary-test-working-directory" value="_32bit::/data/nativetest/" />
+        <option name="binary-test-working-directory" value="_64bit::/data/nativetest64/" />
+        <option name="binary-test-source" value="_32bit::DATA/nativetest/vts_treble_vintf_test/vts_treble_vintf_test" />
+        <option name="binary-test-source" value="_64bit::DATA/nativetest64/vts_treble_vintf_test/vts_treble_vintf_test" />
+        <option name="binary-test-source" value="target_with_delim->/path/to/target_with_delim" />
+        <option name="binary-test-source" value="out/dir/target" />
+        <option name="binary-test-type" value="gtest"/>
+        <option name="test-timeout" value="5m"/>
+    </test>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DeviceInfoCollector">
+        <option name="apk" value="CtsDeviceInfo.apk"/>
+        <option name="test-file-name" value="DeviceHealthTests.apk" />
+        <option name="test-file-name" value="DATA/app/sl4a/sl4a.apk" />
+    </target_preparer>
+</configuration>
diff --git a/atest/unittest_data/cache_root/78ea54ef315f5613f7c11dd1a87f10c7.cache b/atest/unittest_data/cache_root/78ea54ef315f5613f7c11dd1a87f10c7.cache
new file mode 100644
index 0000000..3b384c7
--- /dev/null
+++ b/atest/unittest_data/cache_root/78ea54ef315f5613f7c11dd1a87f10c7.cache
@@ -0,0 +1,81 @@
+c__builtin__
+set
+p0
+((lp1
+ccopy_reg
+_reconstructor
+p2
+(ctest_finders.test_info
+TestInfo
+p3
+c__builtin__
+object
+p4
+Ntp5
+Rp6
+(dp7
+S'compatibility_suites'
+p8
+(lp9
+S'device-tests'
+p10
+asS'install_locations'
+p11
+g0
+((lp12
+S'device'
+p13
+aS'host'
+p14
+atp15
+Rp16
+sS'test_runner'
+p17
+S'AtestTradefedTestRunner'
+p18
+sS'test_finder'
+p19
+S'MODULE'
+p20
+sS'module_class'
+p21
+(lp22
+VNATIVE_TESTS
+p23
+asS'from_test_mapping'
+p24
+I00
+sS'build_targets'
+p25
+g0
+((lp26
+VMODULES-IN-platform_testing-tests-example-native
+p27
+atp28
+Rp29
+sg14
+I00
+sS'test_name'
+p30
+S'hello_world_test'
+p31
+sS'suite'
+p32
+NsS'data'
+p33
+(dp34
+S'rel_config'
+p35
+Vplatform_testing/tests/example/native/AndroidTest.xml
+p36
+sS'filter'
+p37
+c__builtin__
+frozenset
+p38
+((lp39
+tp40
+Rp41
+ssbatp42
+Rp43
+.
\ No newline at end of file
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/cc_path_testing/PathTesting.cpp b/atest/unittest_data/cc_path_testing/PathTesting.cpp
new file mode 100644
index 0000000..cf29370
--- /dev/null
+++ b/atest/unittest_data/cc_path_testing/PathTesting.cpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+#include <gtest/gtest.h>
+
+#include <stdio.h>
+
+TEST(HelloWorldTest, PrintHelloWorld) {
+    printf("Hello, World!");
+}
+
diff --git a/atest/unittest_data/class_file_path_testing/hello_world_test.cc b/atest/unittest_data/class_file_path_testing/hello_world_test.cc
new file mode 100644
index 0000000..8062618
--- /dev/null
+++ b/atest/unittest_data/class_file_path_testing/hello_world_test.cc
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+#include <gtest/gtest.h>
+
+#include <stdio.h>
+
+TEST_F(HelloWorldTest, PrintHelloWorld) {
+    printf("Hello, World!");
+}
\ No newline at end of file
diff --git a/atest/unittest_data/class_file_path_testing/hello_world_test.cpp b/atest/unittest_data/class_file_path_testing/hello_world_test.cpp
new file mode 100644
index 0000000..8062618
--- /dev/null
+++ b/atest/unittest_data/class_file_path_testing/hello_world_test.cpp
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+#include <gtest/gtest.h>
+
+#include <stdio.h>
+
+TEST_F(HelloWorldTest, PrintHelloWorld) {
+    printf("Hello, World!");
+}
\ 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
new file mode 100644
index 0000000..8e0a999
--- /dev/null
+++ b/atest/unittest_data/class_file_path_testing/hello_world_test.java
@@ -0,0 +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/class_file_path_testing/hello_world_test.kt b/atest/unittest_data/class_file_path_testing/hello_world_test.kt
new file mode 100644
index 0000000..623b4a2
--- /dev/null
+++ b/atest/unittest_data/class_file_path_testing/hello_world_test.kt
@@ -0,0 +1 @@
+package com.test.hello_world_test
\ No newline at end of file
diff --git a/atest/unittest_data/class_file_path_testing/hello_world_test.other b/atest/unittest_data/class_file_path_testing/hello_world_test.other
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/unittest_data/class_file_path_testing/hello_world_test.other
diff --git a/atest/unittest_data/gts_auth_key.json b/atest/unittest_data/gts_auth_key.json
new file mode 100644
index 0000000..0e48d55
--- /dev/null
+++ b/atest/unittest_data/gts_auth_key.json
@@ -0,0 +1,8 @@
+{
+  "type": "service_account",
+  "project_id": "test",
+  "private_key_id": "test",
+  "private_key": "test",
+  "client_email": "test",
+  "client_id": "test"
+}
diff --git a/atest/unittest_data/integration_dir_testing/int_dir1/int_dir_testing.xml b/atest/unittest_data/integration_dir_testing/int_dir1/int_dir_testing.xml
new file mode 100644
index 0000000..2dd30f9
--- /dev/null
+++ b/atest/unittest_data/integration_dir_testing/int_dir1/int_dir_testing.xml
@@ -0,0 +1,3 @@
+<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+  <option name="test-file-name" value="CtsUiDeviceTestCases.apk" />
+</target_preparer>
diff --git a/atest/unittest_data/integration_dir_testing/int_dir2/int_dir_testing.xml b/atest/unittest_data/integration_dir_testing/int_dir2/int_dir_testing.xml
new file mode 100644
index 0000000..2dd30f9
--- /dev/null
+++ b/atest/unittest_data/integration_dir_testing/int_dir2/int_dir_testing.xml
@@ -0,0 +1,3 @@
+<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+  <option name="test-file-name" value="CtsUiDeviceTestCases.apk" />
+</target_preparer>
diff --git a/atest/unittest_data/module-info.json b/atest/unittest_data/module-info.json
new file mode 100644
index 0000000..0959fad
--- /dev/null
+++ b/atest/unittest_data/module-info.json
@@ -0,0 +1,19 @@
+{
+  "AmSlam": { "class": ["APPS"],  "path": ["foo/bar/AmSlam"],  "tags": ["tests"],  "installed": ["out/target/product/generic/data/app/AmSlam/AmSlam.apk"], "module_name": "AmSlam" },
+  "CtsJankDeviceTestCases": { "class": ["APPS"],  "path": ["foo/bar/jank"],  "tags": ["optional"],  "installed": ["out/target/product/generic/data/app/CtsJankDeviceTestCases/CtsJankDeviceTestCases.apk"], "module_name": "CtsJankDeviceTestCases" },
+  "CtsUiDeviceTestCases": { "class": ["APPS"],  "path": ["tf/core/CtsUiDeviceTestCases"],  "tags": ["optional"],  "installed": ["out/target/product/generic/data/app/CtsUiDeviceTestCases/CtsUiDeviceTestCases.apk"], "module_name": "CtsJankDeviceTestCases" },
+  "VtsTarget": { "class": ["FAKE"],  "path": ["foo/bar/jank"],  "tags": ["optional"],  "installed": ["out/target/product/generic/VtsTarget"], "module_name": "VtsTarget" },
+  "google-tradefed": { "class": ["JAVA_LIBRARIES"],  "path": ["gtf/core"],  "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/google-tradefed.jar"], "module_name": "google-tradefed" },
+  "google-tradefed-contrib": { "class": ["JAVA_LIBRARIES"],  "path": ["gtf/contrib"],  "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/google-tradefed-contrib.jar"], "module_name": "google-tradefed-contrib" },
+  "tradefed": { "class": ["EXECUTABLES",  "JAVA_LIBRARIES"],  "path": ["tf/core"],  "tags": ["optional"],  "installed": ["out/host/linux-x86/bin/tradefed.sh",  "out/host/linux-x86/framework/tradefed.jar"], "module_name": "tradefed" },
+  "tradefed-contrib": { "class": ["JAVA_LIBRARIES"],  "path": ["tf/contrib"],  "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "tradefed-contrib" },
+  "module-no-path": { "class": ["JAVA_LIBRARIES"],  "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": ["module-no-path"] },
+  "module1": { "class": ["JAVA_LIBRARIES"],  "path": ["shared/path/to/be/used"], "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "module1" },
+  "module2": { "class": ["JAVA_LIBRARIES"],  "path": ["shared/path/to/be/used"], "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "module2" },
+  "multiarch1": { "class": ["JAVA_LIBRARIES"],  "path": ["shared/path/to/be/used2"], "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "multiarch1" },
+  "multiarch1_32": { "class": ["JAVA_LIBRARIES"],  "path": ["shared/path/to/be/used2"], "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "multiarch1" },
+  "multiarch2": { "class": ["JAVA_LIBRARIES"],  "path": ["shared/path/to/be/used2"], "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "multiarch2" },
+  "multiarch2_32": { "class": ["JAVA_LIBRARIES"],  "path": ["shared/path/to/be/used2"], "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "multiarch2" },
+  "multiarch3": { "class": ["JAVA_LIBRARIES"],  "path": ["shared/path/to/be/used2"], "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "multiarch3" },
+  "multiarch3_32": { "class": ["JAVA_LIBRARIES"],  "path": ["shared/path/to/be/used2"], "tags": ["optional"],  "installed": ["out/host/linux-x86/framework/tradefed-contrib.jar"], "module_name": "multiarch3_32" }
+}
diff --git a/atest/unittest_data/path_testing/PathTesting.java b/atest/unittest_data/path_testing/PathTesting.java
new file mode 100644
index 0000000..468307a
--- /dev/null
+++ b/atest/unittest_data/path_testing/PathTesting.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2017 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 android.jank.cts.ui;
+
+/** Dummy Class file for unit tests. */
+public class SomeClassForTesting {
+    private static final String SOME_DUMMY_VAR = "For testing purposes";
+}
diff --git a/atest/unittest_data/path_testing_empty/.empty_file b/atest/unittest_data/path_testing_empty/.empty_file
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/unittest_data/path_testing_empty/.empty_file
diff --git a/atest/unittest_data/test_config/a.xml b/atest/unittest_data/test_config/a.xml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atest/unittest_data/test_config/a.xml
diff --git a/atest/unittest_data/test_mapping/folder1/test_mapping_sample b/atest/unittest_data/test_mapping/folder1/test_mapping_sample
new file mode 100644
index 0000000..05cea61
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder1/test_mapping_sample
@@ -0,0 +1,22 @@
+{
+  "presubmit": [
+    {
+      "name": "test2"
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "test3"
+    }
+  ],
+  "other_group": [
+    {
+      "name": "test4"
+    }
+  ],
+  "imports": [
+    {
+      "path": "../folder2"
+    }
+  ]
+}
diff --git a/atest/unittest_data/test_mapping/folder2/test_mapping_sample b/atest/unittest_data/test_mapping/folder2/test_mapping_sample
new file mode 100644
index 0000000..7517cd5
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder2/test_mapping_sample
@@ -0,0 +1,23 @@
+{
+  "presubmit": [
+    {
+      "name": "test5"
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "test6"
+    }
+  ],
+  "imports": [
+    {
+      "path": "../folder1"
+    },
+    {
+      "path": "../folder3/folder4"
+    },
+    {
+      "path": "../folder3/non-existing"
+    }
+  ]
+}
diff --git a/atest/unittest_data/test_mapping/folder3/folder4/test_mapping_sample b/atest/unittest_data/test_mapping/folder3/folder4/test_mapping_sample
new file mode 100644
index 0000000..6310055
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder3/folder4/test_mapping_sample
@@ -0,0 +1,7 @@
+{
+  "imports": [
+    {
+      "path": "../../folder5"
+    }
+  ]
+}
diff --git a/atest/unittest_data/test_mapping/folder3/test_mapping_sample b/atest/unittest_data/test_mapping/folder3/test_mapping_sample
new file mode 100644
index 0000000..ecd5b7d
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder3/test_mapping_sample
@@ -0,0 +1,17 @@
+{
+  "presubmit": [
+    {
+      "name": "test7"
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "test8"
+    }
+  ],
+  "imports": [
+    {
+      "path": "../folder1"
+    }
+  ]
+}
diff --git a/atest/unittest_data/test_mapping/folder5/test_mapping_sample b/atest/unittest_data/test_mapping/folder5/test_mapping_sample
new file mode 100644
index 0000000..c449a0a
--- /dev/null
+++ b/atest/unittest_data/test_mapping/folder5/test_mapping_sample
@@ -0,0 +1,12 @@
+{
+  "presubmit": [
+    {
+      "name": "test9"
+    }
+  ],
+  "postsubmit": [
+    {
+      "name": "test10"
+    }
+  ]
+}
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/atest/unittest_data/test_mapping/test_mapping_sample b/atest/unittest_data/test_mapping/test_mapping_sample
new file mode 100644
index 0000000..a4edd9c
--- /dev/null
+++ b/atest/unittest_data/test_mapping/test_mapping_sample
@@ -0,0 +1,8 @@
+{
+  "presubmit": [
+    {
+      "name": "test1",
+      "host": true
+    }
+  ]
+}
diff --git a/atest/unittest_data/vts_plan_files/vts-aa.xml b/atest/unittest_data/vts_plan_files/vts-aa.xml
new file mode 100644
index 0000000..629005c
--- /dev/null
+++ b/atest/unittest_data/vts_plan_files/vts-aa.xml
@@ -0,0 +1,4 @@
+<configuration description="VTS Serving Plan for Staging(new) tests">
+  <include name="vts-bb" />
+  <include name="vts-dd" />
+</configuration>
diff --git a/atest/unittest_data/vts_plan_files/vts-bb.xml b/atest/unittest_data/vts_plan_files/vts-bb.xml
new file mode 100644
index 0000000..87c7588
--- /dev/null
+++ b/atest/unittest_data/vts_plan_files/vts-bb.xml
@@ -0,0 +1,3 @@
+<configuration description="VTS Serving Plan for Staging(new) tests">
+  <include name="vts-cc" />
+</configuration>
diff --git a/atest/unittest_data/vts_plan_files/vts-cc.xml b/atest/unittest_data/vts_plan_files/vts-cc.xml
new file mode 100644
index 0000000..14125c0
--- /dev/null
+++ b/atest/unittest_data/vts_plan_files/vts-cc.xml
@@ -0,0 +1,2 @@
+<configuration description="Common preparer">
+</configuration>
diff --git a/atest/unittest_data/vts_plan_files/vts-dd.xml b/atest/unittest_data/vts_plan_files/vts-dd.xml
new file mode 100644
index 0000000..a56597b
--- /dev/null
+++ b/atest/unittest_data/vts_plan_files/vts-dd.xml
@@ -0,0 +1,2 @@
+<configuration description="VTS Serving Plan for Staging(new) tests">
+</configuration>
diff --git a/atest/unittest_data/vts_plan_files/vts-staging-default.xml b/atest/unittest_data/vts_plan_files/vts-staging-default.xml
new file mode 100644
index 0000000..34cccce
--- /dev/null
+++ b/atest/unittest_data/vts_plan_files/vts-staging-default.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configuration description="VTS Serving Plan for Staging(new) tests">
+  <include name="vts-aa" />
+</configuration>
diff --git a/atest/unittest_data/vts_push_files/push_file1.push b/atest/unittest_data/vts_push_files/push_file1.push
new file mode 100644
index 0000000..b55f453
--- /dev/null
+++ b/atest/unittest_data/vts_push_files/push_file1.push
@@ -0,0 +1,4 @@
+push_file1_target1->/path/to/push/push_file1_target1
+push_file1_target2->/path/to/push/push_file1_target2
+
+push_file2.push
diff --git a/atest/unittest_data/vts_push_files/push_file2.push b/atest/unittest_data/vts_push_files/push_file2.push
new file mode 100644
index 0000000..3c5ae78
--- /dev/null
+++ b/atest/unittest_data/vts_push_files/push_file2.push
@@ -0,0 +1,2 @@
+push_file2_target1->/path/to/push_file2_target1
+push_file2_target2->/path/to/push_file2_target2
diff --git a/atest/unittest_utils.py b/atest/unittest_utils.py
new file mode 100644
index 0000000..a57afac
--- /dev/null
+++ b/atest/unittest_utils.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+#
+# Copyright 2017, 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.
+
+"""Utility functions for unit tests."""
+
+import os
+
+import constants
+import unittest_constants as uc
+
+def assert_strict_equal(test_class, first, second):
+    """Check for strict equality and strict equality of nametuple elements.
+
+    assertEqual considers types equal to their subtypes, but we want to
+    not consider set() and frozenset() equal for testing.
+    """
+    test_class.assertEqual(first, second)
+    # allow byte and unicode string equality.
+    if not (isinstance(first, basestring) and
+            isinstance(second, basestring)):
+        test_class.assertIsInstance(first, type(second))
+        test_class.assertIsInstance(second, type(first))
+    # Recursively check elements of namedtuples for strict equals.
+    if isinstance(first, tuple) and hasattr(first, '_fields'):
+        # pylint: disable=invalid-name
+        for f in first._fields:
+            assert_strict_equal(test_class, getattr(first, f),
+                                getattr(second, f))
+
+def assert_equal_testinfos(test_class, test_info_a, test_info_b):
+    """Check that the passed in TestInfos are equal."""
+    # Use unittest.assertEqual to do checks when None is involved.
+    if test_info_a is None or test_info_b is None:
+        test_class.assertEqual(test_info_a, test_info_b)
+        return
+
+    for attr in test_info_a.__dict__:
+        test_info_a_attr = getattr(test_info_a, attr)
+        test_info_b_attr = getattr(test_info_b, attr)
+        test_class.assertEqual(test_info_a_attr, test_info_b_attr,
+                               msg=('TestInfo.%s mismatch: %s != %s' %
+                                    (attr, test_info_a_attr, test_info_b_attr)))
+
+def assert_equal_testinfo_sets(test_class, test_info_set_a, test_info_set_b):
+    """Check that the sets of TestInfos are equal."""
+    test_class.assertEqual(len(test_info_set_a), len(test_info_set_b),
+                           msg=('mismatch # of TestInfos: %d != %d' %
+                                (len(test_info_set_a), len(test_info_set_b))))
+    # Iterate over a set and pop them out as you compare them.
+    while test_info_set_a:
+        test_info_a = test_info_set_a.pop()
+        test_info_b_to_remove = None
+        for test_info_b in test_info_set_b:
+            try:
+                assert_equal_testinfos(test_class, test_info_a, test_info_b)
+                test_info_b_to_remove = test_info_b
+                break
+            except AssertionError:
+                pass
+        if test_info_b_to_remove:
+            test_info_set_b.remove(test_info_b_to_remove)
+        else:
+            # We haven't found a match, raise an assertion error.
+            raise AssertionError('No matching TestInfo (%s) in [%s]' %
+                                 (test_info_a, ';'.join([str(t) for t in test_info_set_b])))
+
+
+def isfile_side_effect(value):
+    """Mock return values for os.path.isfile."""
+    if value == '/%s/%s' % (uc.CC_MODULE_DIR, constants.MODULE_CONFIG):
+        return True
+    if value == '/%s/%s' % (uc.MODULE_DIR, constants.MODULE_CONFIG):
+        return True
+    if value.endswith('.cc'):
+        return True
+    if value.endswith('.cpp'):
+        return True
+    if value.endswith('.java'):
+        return True
+    if value.endswith('.kt'):
+        return True
+    if value.endswith(uc.INT_NAME + '.xml'):
+        return True
+    if value.endswith(uc.GTF_INT_NAME + '.xml'):
+        return True
+    return False
+
+
+def realpath_side_effect(path):
+    """Mock return values for os.path.realpath."""
+    return os.path.join(uc.ROOT, path)