Merge "Remove the config files from core."
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 5138253..3303bfe 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -7,6 +7,10 @@
           "instrumentation-arg": "annotation:=android.support.test.filters.SmallTest"
         }
       ]
+    },
+    {
+      "name": "hello_world_test",
+      "host": true
     }
   ]
 }
diff --git a/atest/atest.py b/atest/atest.py
index 148751c..f0c8fa4 100755
--- a/atest/atest.py
+++ b/atest/atest.py
@@ -30,6 +30,7 @@
 import sys
 import tempfile
 import time
+import platform
 
 import atest_arg_parser
 import atest_metrics
@@ -40,6 +41,8 @@
 import module_info
 import result_reporter
 import test_runner_handler
+from metrics import metrics
+from metrics import metrics_utils
 from test_runners import regression_test_runner
 
 EXPECTED_VARS = frozenset([
@@ -355,7 +358,12 @@
     _configure_logging(args.verbose)
     _validate_args(args)
     atest_metrics.log_start_event()
-
+    start = time.time()
+    metrics.AtestStartEvent(
+        command_line=' '.join(argv),
+        test_references=args.tests,
+        cwd=os.getcwd(),
+        os=platform.platform())
     results_dir = make_test_run_dir()
     mod_info = module_info.ModuleInfo(force_build=args.rebuild_module_info)
     translator = cli_translator.CLITranslator(module_info=mod_info)
@@ -364,9 +372,11 @@
     if _will_run_tests(args):
         build_targets, test_infos = translator.translate(args)
         if not test_infos:
+            metrics_utils.send_exit_event(start, constants.EXIT_CODE_TEST_NOT_FOUND)
             return constants.EXIT_CODE_TEST_NOT_FOUND
         args = _validate_exec_mode(args, test_infos)
     if args.info:
+        metrics_utils.send_exit_event(start, constants.EXIT_CODE_SUCCESS)
         return _print_test_info(mod_info, test_infos)
     build_targets |= test_runner_handler.get_test_runner_reqs(mod_info,
                                                               test_infos)
@@ -382,6 +392,7 @@
         build_targets.add(mod_info.module_info_target)
         success = atest_utils.build(build_targets, args.verbose)
         if not success:
+            metrics_utils.send_exit_event(start, constants.EXIT_CODE_BUILD_FAILURE)
             return constants.EXIT_CODE_BUILD_FAILURE
     elif constants.TEST_STEP not in steps:
         logging.warn('Install step without test step currently not '
@@ -399,6 +410,7 @@
             None, regression_args, reporter)
     if tests_exit_code != constants.EXIT_CODE_SUCCESS:
         tests_exit_code = constants.EXIT_CODE_TEST_FAILURE
+    metrics_utils.send_exit_event(start, tests_exit_code)
     return tests_exit_code
 
 if __name__ == '__main__':
diff --git a/atest/atest_utils.py b/atest/atest_utils.py
index 7925cac..9b94ea7 100644
--- a/atest/atest_utils.py
+++ b/atest/atest_utils.py
@@ -34,7 +34,7 @@
 
 _MAKE_CMD = '%s/build/soong/soong_ui.bash' % os.environ.get(
     constants.ANDROID_BUILD_TOP)
-_BUILD_CMD = [_MAKE_CMD, '--make-mode']
+BUILD_CMD = [_MAKE_CMD, '--make-mode']
 _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
@@ -142,7 +142,7 @@
     print('\n%s\n%s' % (colorize("Building Dependencies...", constants.CYAN),
                         ', '.join(build_targets)))
     logging.debug('Building Dependencies: %s', ' '.join(build_targets))
-    cmd = _BUILD_CMD + list(build_targets)
+    cmd = BUILD_CMD + list(build_targets)
     logging.debug('Executing command: %s', cmd)
     try:
         if verbose:
@@ -277,3 +277,18 @@
         print(output)
     else:
         print(output, end="")
+
+
+def is_external_run():
+    """Check is external run or not.
+
+    Returns:
+        True if this is an external run, False otherwise.
+    """
+    try:
+        output = subprocess.check_output(['git', 'config', '--get', 'user.email'])
+        if output and output.strip().endswith(constants.INTERNAL_EMAIL):
+            return False
+    except subprocess.CalledProcessError:
+        return True
+    return True
diff --git a/atest/constants_default.py b/atest/constants_default.py
index 245a8e2..d5fb3ea 100644
--- a/atest/constants_default.py
+++ b/atest/constants_default.py
@@ -116,6 +116,9 @@
 
 # Metrics
 METRICS_URL = 'http://asuite-218222.appspot.com/atest/metrics'
+EXTERNAL = 'EXTERNAL_RUN'
+INTERNAL = 'INTERNAL_RUN'
+INTERNAL_EMAIL = '@google.com'
 
 # VTS plans
 VTS_STAGING_PLAN = 'vts-staging-default'
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..39e2745
--- /dev/null
+++ b/atest/metrics/clearcut_client.py
@@ -0,0 +1,165 @@
+# 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
+import urllib2
+
+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 = long(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 = urllib2.Request(self._clearcut_url, data=data)
+        try:
+            response = urllib2.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 urllib2.HTTPError as e:
+            logging.debug('Failed to push events to Clearcut. Error code: %d',
+                          e.code)
+        except urllib2.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..4a7d355
--- /dev/null
+++ b/atest/metrics/metrics.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.
+
+"""
+Metrics class.
+"""
+
+import constants
+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
diff --git a/atest/metrics/metrics_base.py b/atest/metrics/metrics_base.py
new file mode 100644
index 0000000..c19219a
--- /dev/null
+++ b/atest/metrics/metrics_base.py
@@ -0,0 +1,100 @@
+# 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.
+"""
+import random
+import time
+import uuid
+
+import atest_utils
+import asuite_metrics
+import clearcut_client
+import constants
+
+from proto import clientanalytics_pb2
+from proto import external_user_log_pb2
+from proto import internal_user_log_pb2
+
+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
+}
+
+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 = _run_id
+    _user_type = (EXTERNAL_USER if atest_utils.is_external_run()
+                  else INTERNAL_USER)
+    _log_source = ATEST_LOG_SOURCE[_user_type]
+    cc = clearcut_client.Clearcut(_log_source)
+
+    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
+        allowed = ({constants.EXTERNAL} if cls._user_type == EXTERNAL_USER
+                   else {constants.EXTERNAL, constants.INTERNAL})
+        fields = [k for k, v in vars(cls).iteritems()
+                  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,
+                  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 = long((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..8779c66
--- /dev/null
+++ b/atest/metrics/metrics_utils.py
@@ -0,0 +1,55 @@
+# 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 time
+
+import metrics
+
+
+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 = long(diff_time_sec)
+    nanos = int((diff_time_sec - seconds)*10**9)
+    return {'seconds': seconds, 'nanos': nanos}
+
+def send_exit_event(start_time, exit_code, stacktrace='', logs=''):
+    """log exit event and flush all events to clearcut.
+    Args:
+        start_time: Start time in seconds.
+        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()-start_time),
+        exit_code=exit_code,
+        stacktrace=stacktrace,
+        logs=logs)
+    # pylint: disable=no-member
+    clearcut.flush_events()
diff --git a/atest/test_runners/atest_tf_test_runner.py b/atest/test_runners/atest_tf_test_runner.py
index 399be76..611ba95 100644
--- a/atest/test_runners/atest_tf_test_runner.py
+++ b/atest/test_runners/atest_tf_test_runner.py
@@ -130,28 +130,18 @@
         Returns:
             0 if tests succeed, non-zero otherwise.
         """
-        iterations = 1
-        metrics_folder = ''
-        if extra_args.get(constants.PRE_PATCH_ITERATIONS):
-            iterations = extra_args.pop(constants.PRE_PATCH_ITERATIONS)
-            metrics_folder = os.path.join(self.results_dir, 'baseline-metrics')
-        elif extra_args.get(constants.POST_PATCH_ITERATIONS):
-            iterations = extra_args.pop(constants.POST_PATCH_ITERATIONS)
-            metrics_folder = os.path.join(self.results_dir, 'new-metrics')
-        args = self._create_test_args(test_infos)
+        iterations = self._generate_iterations(extra_args)
         reporter.register_unsupported_runner(self.NAME)
 
         ret_code = constants.EXIT_CODE_SUCCESS
         for _ in range(iterations):
-            run_cmd = self._generate_run_command(args, extra_args,
-                                                 metrics_folder)
-            subproc = self.run(run_cmd, output_to_stdout=True)
+            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
 
     # pylint: disable=broad-except
     # pylint: disable=too-many-locals
-    # pylint: disable=too-many-branches
     def run_tests_pretty(self, test_infos, extra_args, reporter):
         """Run the list of test_infos. See base class for more.
 
@@ -163,34 +153,18 @@
         Returns:
             0 if tests succeed, non-zero otherwise.
         """
-        iterations = 1
-        metrics_folder = ''
-        if extra_args.get(constants.PRE_PATCH_ITERATIONS):
-            iterations = extra_args.pop(constants.PRE_PATCH_ITERATIONS)
-            metrics_folder = os.path.join(self.results_dir, 'baseline-metrics')
-        elif extra_args.get(constants.POST_PATCH_ITERATIONS):
-            iterations = extra_args.pop(constants.POST_PATCH_ITERATIONS)
-            metrics_folder = os.path.join(self.results_dir, 'new-metrics')
-        args = self._create_test_args(test_infos)
-        # 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)
-
+        iterations = self._generate_iterations(extra_args)
         ret_code = constants.EXIT_CODE_SUCCESS
         for _ in range(iterations):
             server = self._start_socket_server()
-            run_cmd = self._generate_run_command(args, extra_args,
-                                                 metrics_folder,
-                                                 server.getsockname()[1])
-            subproc = self.run(run_cmd, output_to_stdout=self.is_verbose)
+            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)
             try:
                 signal.signal(signal.SIGINT, self._signal_passer(subproc))
                 conn, addr = self._exec_with_tf_polling(server.accept, subproc)
                 logging.debug('Accepted connection from %s', addr)
                 self._process_connection(conn, reporter)
-                if metrics_folder:
-                    logging.info('Saved metrics in: %s', metrics_folder)
             except Exception as error:
                 # exc_info=1 tells logging to log the stacktrace
                 logging.debug('Caught exception:', exc_info=1)
@@ -523,26 +497,47 @@
             args_not_supported.append(arg)
         return args_to_append, args_not_supported
 
-    def _generate_run_command(self, args, extra_args, metrics_folder,
-                              port=None):
+    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:
-            args: A list of strings of TF arguments to run the tests.
+            test_infos: A set of TestInfo instances.
             extra_args: A Dict of extra args to append.
-            metrics_folder: A string of the filepath to put metrics.
             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 string that contains the atest tradefed run command.
+            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])
 
@@ -554,7 +549,7 @@
 
         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)
+        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
@@ -652,8 +647,16 @@
 
         Returns: A list of TF arguments to run the tests.
         """
-        test_infos = self._flatten_test_infos(test_infos)
         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)
+
         for info in test_infos:
             args.extend([constants.TF_INCLUDE_FILTER, info.test_name])
             filters = set()
diff --git a/atest/test_runners/atest_tf_test_runner_unittest.py b/atest/test_runners/atest_tf_test_runner_unittest.py
index 22decc5..6593e07 100755
--- a/atest/test_runners/atest_tf_test_runner_unittest.py
+++ b/atest/test_runners/atest_tf_test_runner_unittest.py
@@ -174,7 +174,7 @@
     @mock.patch.object(atf_tr.AtestTradefedTestRunner,
                        '_create_test_args', return_value=['some_args'])
     @mock.patch.object(atf_tr.AtestTradefedTestRunner,
-                       '_generate_run_command', return_value='some_cmd')
+                       '_generate_run_commands', return_value='some_cmd')
     @mock.patch.object(atf_tr.AtestTradefedTestRunner,
                        '_process_connection', return_value=None)
     @mock.patch('os.killpg', return_value=None)
@@ -385,26 +385,31 @@
                           self.tr._check_events_are_balanced,
                           name, mock_reporter, state, stack)
 
+
+    @mock.patch.object(atf_tr.AtestTradefedTestRunner, '_generate_metrics_folder')
     @mock.patch('atest_utils.get_result_server_args')
-    def test_generate_run_command(self, mock_resultargs):
+    def test_generate_run_commands(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_command([], {}, ''),
-            RUN_CMD.format(metrics=''))
+            self.tr._generate_run_commands([], {}),
+            [RUN_CMD.format(metrics='')])
+        mock_mertrics.return_value = METRICS_DIR
         unittest_utils.assert_strict_equal(
             self,
-            self.tr._generate_run_command([], {}, METRICS_DIR),
-            RUN_CMD.format(metrics=METRICS_DIR_ARG))
+            self.tr._generate_run_commands([], {}),
+            [RUN_CMD.format(metrics=METRICS_DIR_ARG)])
         # 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_command([], {}, ''),
-            RUN_CMD.format(metrics='') + ' ' + result_arg)
+            self.tr._generate_run_commands([], {}),
+            [RUN_CMD.format(metrics='') + ' ' + result_arg])
 
     def test_flatten_test_filters(self):
         """Test _flatten_test_filters method."""
diff --git a/atest/test_runners/example_test_runner.py b/atest/test_runners/example_test_runner.py
index 56ab31f..3482ce9 100644
--- a/atest/test_runners/example_test_runner.py
+++ b/atest/test_runners/example_test_runner.py
@@ -35,10 +35,8 @@
             extra_args: Dict of extra args to add to test run.
             reporter: An instance of result_report.ResultReporter
         """
-        for test_info in test_infos:
-            run_cmd_dict = {'exe': self.EXECUTABLE,
-                            'test': test_info.test_name}
-            run_cmd = self._RUN_CMD.format(**run_cmd_dict)
+        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):
@@ -57,3 +55,23 @@
             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
index c0a2250..986ac85 100644
--- a/atest/test_runners/regression_test_runner.py
+++ b/atest/test_runners/regression_test_runner.py
@@ -46,13 +46,8 @@
         Returns:
             Return code of the process for running tests.
         """
-        reporter.register_unsupported_runner(self.NAME)
-        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)
-        proc = super(RegressionTestRunner, self).run(run_cmd,
+        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
@@ -73,3 +68,24 @@
             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
index 9a4b86c..03e329f 100644
--- a/atest/test_runners/robolectric_test_runner.py
+++ b/atest/test_runners/robolectric_test_runner.py
@@ -20,6 +20,7 @@
 """
 
 import logging
+import os
 
 # pylint: disable=import-error
 import atest_utils
@@ -105,3 +106,24 @@
             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.BUILD_CMD + [str(test_info.test_name)]
+            run_cmd = ' '.join(x for x in robo_command).replace(
+                os.environ.get(constants.ANDROID_BUILD_TOP) + os.sep, '')
+            run_cmds.append(run_cmd)
+        return run_cmds
diff --git a/atest/test_runners/test_runner_base.py b/atest/test_runners/test_runner_base.py
index 321044e..9e29216 100644
--- a/atest/test_runners/test_runner_base.py
+++ b/atest/test_runners/test_runner_base.py
@@ -138,3 +138,17 @@
     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/pylintrc b/pylintrc
index ccbc81f..fb021fb 100644
--- a/pylintrc
+++ b/pylintrc
@@ -7,6 +7,10 @@
     fixme,no-self-use,
     duplicate-code
 
+[MASTER]
+
+init-hook='import sys, os; sys.path.append(os.getcwd() + '/atest')'
+
 [BASIC]
 
 # Naming hint for method names
diff --git a/res/apks/telephonyutil/MODULE_LICENSE_APL b/res/apks/telephonyutil/MODULE_LICENSE_APL
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/res/apks/telephonyutil/MODULE_LICENSE_APL
diff --git a/res/apks/telephonyutil/PREBUILT b/res/apks/telephonyutil/PREBUILT
new file mode 100644
index 0000000..bab0a07
--- /dev/null
+++ b/res/apks/telephonyutil/PREBUILT
@@ -0,0 +1,4 @@
+This apk can be rebuilt from
+        platform/platform_testing
+
+By running `m TelephonyUtility` on revision e295cf9c1b0c4db11a41bdcbe220f7179b075b8a
diff --git a/res/apks/telephonyutil/TelephonyUtility.apk b/res/apks/telephonyutil/TelephonyUtility.apk
new file mode 100644
index 0000000..799374d
--- /dev/null
+++ b/res/apks/telephonyutil/TelephonyUtility.apk
Binary files differ
diff --git a/res/config/regression.xml b/res/config/regression.xml
deleted file mode 100644
index 4643969..0000000
--- a/res/config/regression.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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="Runs a regression detection algorithm on two sets of metrics">
-
-    <option name="log-level" value="verbose" />
-    <option name="compress-files" value="false" />
-    <test class="com.android.tradefed.testtype.metricregression.DetectRegression" />
-    <logger class="com.android.tradefed.log.FileLogger" />
-    <result_reporter class="com.android.tradefed.result.ConsoleResultReporter" />
-
-</configuration>
diff --git a/src/com/android/tradefed/build/BuildInfoKey.java b/src/com/android/tradefed/build/BuildInfoKey.java
index c9248cd..05663bd 100644
--- a/src/com/android/tradefed/build/BuildInfoKey.java
+++ b/src/com/android/tradefed/build/BuildInfoKey.java
@@ -15,6 +15,9 @@
  */
 package com.android.tradefed.build;
 
+import java.util.HashSet;
+import java.util.Set;
+
 /** Class holding enumeration related to build information queries. */
 public class BuildInfoKey {
 
@@ -41,6 +44,9 @@
         TARGET_LINKED_DIR("target_testcases", false),
         HOST_LINKED_DIR("host_testcases", false),
 
+        // A folder containing the resources from isFake=true devices
+        SHARED_RESOURCE_DIR("resources_dir", false),
+
         // Keys that can hold lists of files.
         PACKAGE_FILES("package_files", true);
 
@@ -78,4 +84,16 @@
             return null;
         }
     }
+
+    /**
+     * Files key that should be shared from a resources build info to all build infos via the {@link
+     * IDeviceBuildInfo#getResourcesDir()}.
+     */
+    public static final Set<BuildInfoFileKey> SHARED_KEY = new HashSet<>();
+
+    static {
+        SHARED_KEY.add(BuildInfoFileKey.PACKAGE_FILES);
+        SHARED_KEY.add(BuildInfoFileKey.TESTDIR_IMAGE);
+        SHARED_KEY.add(BuildInfoFileKey.ROOT_DIRECTORY);
+    }
 }
diff --git a/src/com/android/tradefed/build/DeviceBuildInfo.java b/src/com/android/tradefed/build/DeviceBuildInfo.java
index a556832..e4e0593 100644
--- a/src/com/android/tradefed/build/DeviceBuildInfo.java
+++ b/src/com/android/tradefed/build/DeviceBuildInfo.java
@@ -133,6 +133,18 @@
         setFile(BuildInfoFileKey.TESTDIR_IMAGE, testsDir, version);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public File getResourcesDir() {
+        return getFile(BuildInfoFileKey.SHARED_RESOURCE_DIR);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setResourcesDir(File resourceDir, String version) {
+        setFile(BuildInfoFileKey.SHARED_RESOURCE_DIR, resourceDir, version);
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/build/FileDownloadCache.java b/src/com/android/tradefed/build/FileDownloadCache.java
index 771230b..2f06bcd 100644
--- a/src/com/android/tradefed/build/FileDownloadCache.java
+++ b/src/com/android/tradefed/build/FileDownloadCache.java
@@ -26,7 +26,6 @@
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
@@ -50,17 +49,17 @@
 
     /**
      * The map of remote file paths to local files, stored in least-recently-used order.
-     * <p/>
-     * Used for performance reasons. Functionally speaking, this data structure is not needed,
+     *
+     * <p>Used for performance reasons. Functionally speaking, this data structure is not needed,
      * since all info could be obtained from inspecting the filesystem.
      */
-    private final Map<String, File> mCacheMap = new LinkedHashMap<String, File>();
+    private final Map<String, File> mCacheMap = new CollapsedKeyMap<>();
 
     /** the lock for <var>mCacheMap</var> */
     private final ReentrantLock mCacheMapLock = new ReentrantLock();
 
     /** A map of remote file paths to locks. */
-    private final Map<String, ReentrantLock> mFileLocks = new HashMap<String, ReentrantLock>();
+    private final Map<String, ReentrantLock> mFileLocks = new CollapsedKeyMap<>();
 
     private long mCurrentCacheSize = 0;
 
@@ -468,4 +467,31 @@
             unlockFile(remoteFilePath);
         }
     }
+
+    /**
+     * Class that ensure the remote file path as the key is always similar to an actual folder
+     * hierarchy.
+     */
+    private static class CollapsedKeyMap<V> extends LinkedHashMap<String, V> {
+        @Override
+        public V put(String key, V value) {
+            return super.put(new File(key).getPath(), value);
+        }
+
+        @Override
+        public V get(Object key) {
+            if (key instanceof String) {
+                return super.get(new File((String) key).getPath());
+            }
+            return super.get(key);
+        }
+
+        @Override
+        public V remove(Object key) {
+            if (key instanceof String) {
+                return super.remove(new File((String) key).getPath());
+            }
+            return super.remove(key);
+        }
+    }
 }
diff --git a/src/com/android/tradefed/build/IDeviceBuildInfo.java b/src/com/android/tradefed/build/IDeviceBuildInfo.java
index 0cfd369..73dafb0 100644
--- a/src/com/android/tradefed/build/IDeviceBuildInfo.java
+++ b/src/com/android/tradefed/build/IDeviceBuildInfo.java
@@ -79,6 +79,22 @@
     public String getTestsDirVersion();
 
     /**
+     * Returns the dir containing some of the downloaded resources. (Resources are usually
+     * associated with a isFake=true device definition). Returns null if no resource dir available.
+     */
+    public default File getResourcesDir() {
+        return null;
+    }
+
+    /**
+     * Sets the resources directory {@link File}.
+     *
+     * @param resourcesDir The directory containing the shared resources.
+     * @param version The version of the directory file.
+     */
+    public default void setResourcesDir(File resourcesDir, String version) {}
+
+    /**
      * Set local path to the extracted tests.zip file contents.
      *
      * @param testsZipFile
diff --git a/src/com/android/tradefed/build/OtaDeviceBuildInfo.java b/src/com/android/tradefed/build/OtaDeviceBuildInfo.java
index b413bde..1a8141b 100644
--- a/src/com/android/tradefed/build/OtaDeviceBuildInfo.java
+++ b/src/com/android/tradefed/build/OtaDeviceBuildInfo.java
@@ -293,6 +293,18 @@
         mBaselineBuild.setTestsDir(testsZipFile, version);
     }
 
+    /** {@inheritDoc} */
+    @Override
+    public File getResourcesDir() {
+        return mBaselineBuild.getResourcesDir();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setResourcesDir(File resourceDir, String version) {
+        mBaselineBuild.setResourcesDir(resourceDir, version);
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/src/com/android/tradefed/command/CommandInterrupter.java b/src/com/android/tradefed/command/CommandInterrupter.java
index 030705b..bb035f7 100644
--- a/src/com/android/tradefed/command/CommandInterrupter.java
+++ b/src/com/android/tradefed/command/CommandInterrupter.java
@@ -29,7 +29,6 @@
 import java.util.concurrent.TimeUnit;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 
 /** Service allowing TradeFederation commands to be interrupted or marked as uninterruptible. */
 public class CommandInterrupter {
@@ -101,13 +100,13 @@
      * Flag a thread, interrupting it if and when it becomes interruptible.
      *
      * @param thread thread to mark for interruption
-     * @param template interruption error template
-     * @param args interruption error arguments
+     * @param message interruption message
      */
     // FIXME: reduce visibility once RunUtil interrupt methods are removed
-    public void interrupt(
-            @Nonnull Thread thread, @Nullable String template, @Nullable Object... args) {
-        String message = String.format(String.valueOf(template), args);
+    public void interrupt(@Nonnull Thread thread, @Nonnull String message) {
+        if (message == null) {
+            throw new IllegalArgumentException("message cannot be null.");
+        }
         mInterruptMessage.put(thread, message);
         if (isInterruptible(thread)) {
             thread.interrupt();
diff --git a/src/com/android/tradefed/config/OptionSetter.java b/src/com/android/tradefed/config/OptionSetter.java
index 9d27db6..a071cf0 100644
--- a/src/com/android/tradefed/config/OptionSetter.java
+++ b/src/com/android/tradefed/config/OptionSetter.java
@@ -757,6 +757,7 @@
         return unsetOptions;
     }
 
+    @SuppressWarnings("unchecked")
     protected Set<File> validateGcsFilePath() throws ConfigurationException {
         Set<File> gcsFiles = new HashSet<>();
         for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
@@ -788,31 +789,41 @@
                     continue;
                 } else if (value instanceof File) {
                     File consideredFile = (File) value;
-                    // Don't use absolute path as it would not start with gs:
-                    if (consideredFile.getPath().startsWith(GCSFileDownloader.GCS_APPROX_PREFIX)
-                            || consideredFile.getPath().startsWith(GCSFileDownloader.GCS_PREFIX)) {
-                        String path = consideredFile.getPath();
-                        CLog.d(
-                                "Considering option '%s' with path: '%s' for download.",
-                                option.name(), path);
-                        // We need to download the file from the bucket
+                    File downloadedFile =
+                            resolveGcsFiles(consideredFile, downloader, option, gcsFiles);
+                    if (downloadedFile != null) {
+                        // Replace the field value
                         try {
-                            File gsFile = downloader.fetchTestResource(path);
-                            gcsFiles.add(gsFile);
-                            // Replace the field value
-                            field.set(obj, gsFile);
-                        } catch (BuildRetrievalError | IllegalAccessException e) {
+                            field.set(obj, downloadedFile);
+                        } catch (IllegalAccessException e) {
                             CLog.e(e);
                             // Clean up all files
                             for (File f : gcsFiles) {
                                 FileUtil.deleteFile(f);
                             }
                             throw new ConfigurationException(
-                                    String.format("Failed to download %s", path), e);
+                                    String.format(
+                                            "Failed to download %s", consideredFile.getPath()),
+                                    e);
+                        }
+                    }
+                } else if (value instanceof Collection) {
+                    Collection c = (Collection) value;
+                    Collection copy = new ArrayList<>(c);
+                    for (Object o : copy) {
+                        if (o instanceof File) {
+                            File consideredFile = (File) o;
+                            File downloadedFile =
+                                    resolveGcsFiles(consideredFile, downloader, option, gcsFiles);
+                            if (downloadedFile != null) {
+                                // TODO: See if order could be preserved.
+                                c.remove(consideredFile);
+                                c.add(downloadedFile);
+                            }
                         }
                     }
                 }
-                // TODO: Handle collection of files
+                // TODO: Handle Map of files
             }
         }
         return gcsFiles;
@@ -824,6 +835,31 @@
         return new GCSDownloaderHelper();
     }
 
+    private File resolveGcsFiles(
+            File consideredFile, GCSDownloaderHelper downloader, Option option, Set<File> gcsFiles)
+            throws ConfigurationException {
+        // Don't use absolute path as it would not start with gs:
+        if (consideredFile.getPath().startsWith(GCSFileDownloader.GCS_APPROX_PREFIX)
+                || consideredFile.getPath().startsWith(GCSFileDownloader.GCS_PREFIX)) {
+            String path = consideredFile.getPath();
+            CLog.d("Considering option '%s' with path: '%s' for download.", option.name(), path);
+            // We need to download the file from the bucket
+            try {
+                File gsFile = downloader.fetchTestResource(path);
+                gcsFiles.add(gsFile);
+                return gsFile;
+            } catch (BuildRetrievalError e) {
+                CLog.e(e);
+                // Clean up all files
+                for (File f : gcsFiles) {
+                    FileUtil.deleteFile(f);
+                }
+                throw new ConfigurationException(String.format("Failed to download %s", path), e);
+            }
+        }
+        return null;
+    }
+
     /**
      * Gets a list of all {@link Option} fields (both declared and inherited) for given class.
      *
diff --git a/src/com/android/tradefed/device/TestDevice.java b/src/com/android/tradefed/device/TestDevice.java
index f78d42a..243425c 100644
--- a/src/com/android/tradefed/device/TestDevice.java
+++ b/src/com/android/tradefed/device/TestDevice.java
@@ -1331,7 +1331,8 @@
                 // Line is "Profile owner (User <id>):
                 String[] tokens = line.split("\\(|\\)| ");
                 int userId = Integer.parseInt(tokens[4]);
-                i++;
+
+                i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
                 line = lines[i].trim();
                 // Line is admin=ComponentInfo{<component>}
                 tokens = line.split("\\{|\\}");
@@ -1339,13 +1340,14 @@
                 CLog.d("Cleaning up profile owner " + userId + " " + componentName);
                 removeAdmin(componentName, userId);
             } else if (line.contains("Device Owner:")) {
-                i++;
+                i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
                 line = lines[i].trim();
                 // Line is admin=ComponentInfo{<component>}
                 String[] tokens = line.split("\\{|\\}");
                 String componentName = tokens[1];
+
                 // Skip to user id line.
-                i += 3;
+                i = moveToNextIndexMatchingRegex(".*User ID:.*", lines, i);
                 line = lines[i].trim();
                 // Line is User ID: <N>
                 tokens = line.split(":");
@@ -1357,6 +1359,30 @@
     }
 
     /**
+     * Search forward from the current index to find a string matching the given regex.
+     *
+     * @param regex The regex to match each line against.
+     * @param lines An array of strings to be searched.
+     * @param currentIndex the index to start searching from.
+     * @return The index of a string beginning with the regex.
+     * @throws IllegalStateException if the line cannot be found.
+     */
+    private int moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex) {
+        while (currentIndex < lines.length && !lines[currentIndex].matches(regex)) {
+            currentIndex++;
+        }
+
+        if (currentIndex >= lines.length) {
+            throw new IllegalStateException(
+                    "The output of 'dumpsys device_policy' was not as expected. Owners have not "
+                            + "been removed. This will leave the device in an unstable state and "
+                            + "will lead to further test failures.");
+        }
+
+        return currentIndex;
+    }
+
+    /**
      * Helper for Api level checking of features in the new release before we incremented the api
      * number.
      */
diff --git a/src/com/android/tradefed/device/helper/TelephonyHelper.java b/src/com/android/tradefed/device/helper/TelephonyHelper.java
new file mode 100644
index 0000000..1d4a29a
--- /dev/null
+++ b/src/com/android/tradefed/device/helper/TelephonyHelper.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device.helper;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestResult.TestStatus;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.WifiHelper;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.CollectingTestListener;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.result.TestResult;
+import com.android.tradefed.result.TestRunResult;
+import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** A utility to use and get information related to the telephony. */
+public class TelephonyHelper {
+
+    private static final String TELEPHONY_UTIL_APK_NAME = "TelephonyUtility.apk";
+    private static final String TELEPHONY_APK_RES_PATH = "/apks/telephonyutil/";
+    public static final String PACKAGE_NAME = "android.telephony.utility";
+    private static final String CLASS_NAME = ".SimCardUtil";
+    private static final String METHOD_NAME = "getSimCardInformation";
+
+    private static final String AJUR_RUNNER = "androidx.test.runner.AndroidJUnitRunner";
+
+    public static final String SIM_STATE_KEY = "sim_state";
+    public static final String CARRIER_PRIVILEGES_KEY = "has_carried_privileges";
+
+    public static final TestDescription SIM_TEST =
+            new TestDescription(PACKAGE_NAME + CLASS_NAME, METHOD_NAME);
+
+    /** An information holder for the sim card related information. */
+    public static class SimCardInformation {
+
+        public boolean mHasTelephonySupport;
+        public String mSimState;
+        public boolean mCarrierPrivileges;
+
+        @Override
+        public String toString() {
+            return "SimCardInformation [mHasTelephonySupport="
+                    + mHasTelephonySupport
+                    + ", mSimState="
+                    + mSimState
+                    + ", mCarrierPrivileges="
+                    + mCarrierPrivileges
+                    + "]";
+        }
+    }
+
+    /**
+     * Get the information related to sim card from a given device.
+     *
+     * @param device The device under tests
+     * @return A {@link SimCardInformation} object populated with the sim card info or null if
+     *     anything goes wrong.
+     */
+    public static SimCardInformation getSimInfo(ITestDevice device)
+            throws DeviceNotAvailableException {
+        File apkFile = null;
+        boolean wasInstalled = false;
+        try {
+            apkFile = extractTelephonyUtilApk();
+            if (apkFile == null) {
+                return null;
+            }
+            final String error = device.installPackage(apkFile, true);
+            if (error != null) {
+                CLog.e(error);
+                return null;
+            }
+
+            wasInstalled = true;
+            CollectingTestListener listener = new CollectingTestListener();
+            IRemoteAndroidTestRunner runner = createTestRunner(device.getIDevice());
+            runner.setMethodName(PACKAGE_NAME + CLASS_NAME, METHOD_NAME);
+            device.runInstrumentationTests(runner, listener);
+            TestRunResult runResult = listener.getCurrentRunResults();
+            if (!runResult.isRunComplete()) {
+                CLog.e("Run did not complete.");
+                return null;
+            }
+            if (runResult.isRunFailure()) {
+                CLog.e("TelephonyHelper run failure: %s", runResult.getRunFailureMessage());
+                return null;
+            }
+            TestResult testResult = runResult.getTestResults().get(SIM_TEST);
+            if (testResult == null) {
+                CLog.e("getSimCardInformation did not run");
+                return null;
+            }
+            SimCardInformation info = new SimCardInformation();
+            info.mHasTelephonySupport = !TestStatus.FAILURE.equals(testResult.getStatus());
+            info.mSimState = testResult.getMetrics().get(SIM_STATE_KEY);
+            info.mCarrierPrivileges =
+                    stringToBool(testResult.getMetrics().get(CARRIER_PRIVILEGES_KEY));
+            CLog.d("%s", info);
+            return info;
+        } finally {
+            FileUtil.deleteFile(apkFile);
+            if (wasInstalled) {
+                device.uninstallPackage(PACKAGE_NAME);
+            }
+        }
+    }
+
+    /** Helper method to extract the wifi util apk from the classpath */
+    private static File extractTelephonyUtilApk() {
+        File apkTempFile = null;
+        try {
+            apkTempFile = FileUtil.createTempFile(TELEPHONY_UTIL_APK_NAME, ".apk");
+            InputStream apkStream =
+                    WifiHelper.class.getResourceAsStream(
+                            TELEPHONY_APK_RES_PATH + TELEPHONY_UTIL_APK_NAME);
+            FileUtil.writeToFile(apkStream, apkTempFile);
+        } catch (IOException e) {
+            CLog.e(e);
+            FileUtil.deleteFile(apkTempFile);
+            return null;
+        }
+        return apkTempFile;
+    }
+
+    private static IRemoteAndroidTestRunner createTestRunner(IDevice idevice) {
+        return new DefaultRemoteAndroidTestRunner(PACKAGE_NAME, AJUR_RUNNER, idevice);
+    }
+
+    private static boolean stringToBool(String boolString) {
+        if (boolString == null) {
+            return false;
+        }
+        return Boolean.parseBoolean(boolString);
+    }
+}
diff --git a/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollector.java b/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollector.java
index 0249cf4..8b7a75b 100644
--- a/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollector.java
+++ b/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollector.java
@@ -26,8 +26,8 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.AbstractMap.SimpleEntry;
-import java.util.Arrays;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -60,12 +60,13 @@
     @Override
     public void onTestRunEnd(
             DeviceMetricData runData, final Map<String, Metric> currentRunMetrics) {
-        processMetricRequest(runData, TfMetricProtoUtil.compatibleConvert(currentRunMetrics));
+        processMetricRequest(runData, currentRunMetrics);
     }
 
     @Override
-    public void onTestEnd(DeviceMetricData testData, Map<String, Metric> currentTestCaseMetrics) {
-        processMetricRequest(testData, TfMetricProtoUtil.compatibleConvert(currentTestCaseMetrics));
+    public void onTestEnd(DeviceMetricData testData,
+            Map<String, Metric> currentTestCaseMetrics) {
+        processMetricRequest(testData, currentTestCaseMetrics);
     }
 
     /** Adds additional pattern keys to the pull from the device. */
@@ -79,9 +80,9 @@
      *
      * @param key the option key associated to the file that was pulled.
      * @param metricFile the {@link File} pulled from the device matching the option key.
-     * @param runData the run {@link DeviceMetricData} where metrics can be stored.
+     * @param data the {@link DeviceMetricData} where metrics can be stored.
      */
-    public abstract void processMetricFile(String key, File metricFile, DeviceMetricData runData);
+    public abstract void processMetricFile(String key, File metricFile, DeviceMetricData data);
 
     /**
      * Implementation of the method should allow to log the directory, parse it for metrics to be
@@ -89,12 +90,22 @@
      *
      * @param key the option key associated to the directory that was pulled.
      * @param metricDirectory the {@link File} pulled from the device matching the option key.
-     * @param runData the run {@link DeviceMetricData} where metrics can be stored.
+     * @param data the {@link DeviceMetricData} where metrics can be stored.
      */
     public abstract void processMetricDirectory(
-            String key, File metricDirectory, DeviceMetricData runData);
+            String key, File metricDirectory, DeviceMetricData data);
 
-    private void processMetricRequest(DeviceMetricData data, Map<String, String> currentMetrics) {
+    /**
+     * Process the file associated with the matching key or directory name and update
+     * the data with any additional metrics.
+     *
+     * @param data where the final metrics will be stored.
+     * @param metrics where the key or directory name will be matched to the keys.
+     */
+    private void processMetricRequest(DeviceMetricData data,
+            Map<String, Metric> metrics) {
+        Map<String, String> currentMetrics = TfMetricProtoUtil
+                .compatibleConvert(metrics);
         if (mKeys.isEmpty() && mDirectoryKeys.isEmpty()) {
             return;
         }
@@ -115,9 +126,9 @@
     }
 
     private Entry<String, File> pullMetricFile(
-            String pattern, final Map<String, String> currentRunMetrics) {
+            String pattern, final Map<String, String> currentMetrics) {
         Pattern p = Pattern.compile(pattern);
-        for (Entry<String, String> entry : currentRunMetrics.entrySet()) {
+        for (Entry<String, String> entry : currentMetrics.entrySet()) {
             if (p.matcher(entry.getKey()).find()) {
                 for (ITestDevice device : getDevices()) {
                     try {
diff --git a/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java b/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
new file mode 100644
index 0000000..92c807b
--- /dev/null
+++ b/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollector.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device.metric;
+
+import com.android.annotations.VisibleForTesting;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.metrics.proto.MetricMeasurement.DataType;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base implementation of {@link FilePullerDeviceMetricCollector} that allows
+ * pulling the perfetto files from the device and collect the metrics from it.
+ */
+@OptionClass(alias = "perfetto-metric-collector")
+public class PerfettoPullerMetricCollector extends FilePullerDeviceMetricCollector {
+
+    // Timeout for the script to process the trace files.
+    // This value is arbitarily choosen to be 5 mins to prevent the test spending more time
+    // in processing the files.
+    private static final long MAX_SCRIPT_TIMEOUT_MSECS = 300000;
+    private static final String LINE_SEPARATOR = "\\r?\\n";
+    private static final String KEY_VALUE_SEPARATOR = ":";
+
+    @Option(
+            name = "perfetto-binary-path",
+            description = "Path to the script files used to analyze the trace files.")
+    private List<String> mScriptPaths = new ArrayList<>();
+
+    @Option(
+            name = "perfetto-metric-prefix",
+            description = "Prefix to be used with the metrics collected from perfetto.")
+    private String mMetricPrefix = "perfetto";
+
+    /**
+     * Process the perfetto trace file for the additional metrics and add it to final metrics.
+     *
+     * @param key the option key associated to the file that was pulled from the device.
+     * @param metricFile the {@link File} pulled from the device matching the option key.
+     * @param data where metrics will be stored.
+     */
+    @Override
+    public void processMetricFile(String key, File metricFile,
+            DeviceMetricData data) {
+        // Extract the metrics from the trace file.
+        for (String scriptPath : mScriptPaths) {
+            List<String> commandArgsList = new ArrayList<String>();
+            commandArgsList.add(scriptPath);
+            commandArgsList.add("-trace_file");
+            commandArgsList.add(metricFile.getAbsolutePath());
+
+            CommandResult cr = runHostCommand(commandArgsList.toArray(new String[commandArgsList
+                    .size()]));
+            if (CommandStatus.SUCCESS.equals(cr.getStatus())) {
+                String[] metrics = cr.getStdout().split(LINE_SEPARATOR);
+                for (String metric : metrics) {
+                    // Expected script test outRput format.
+                    // Key1:Value1
+                    // Key2:Value2
+                    String[] pair = metric.split(KEY_VALUE_SEPARATOR);
+                    Metric.Builder metricBuilder = Metric.newBuilder();
+                    metricBuilder
+                            .getMeasurementsBuilder()
+                            .setSingleString(pair[1]);
+                    if (pair.length == 2) {
+                        data.addMetric(String.format("%s_%s", mMetricPrefix, pair[0]),
+                                metricBuilder.setType(DataType.RAW));
+                    } else {
+                        CLog.e("Output %s not in the expected format.", metric);
+                    }
+                }
+                CLog.i(cr.getStdout());
+            } else {
+                CLog.e("Unable to parse the trace file %s due to %s - Status - %s ",
+                        metricFile.getName(), cr.getStderr(), cr.getStatus());
+            }
+        }
+
+        // Upload and delete the host trace file.
+        try (InputStreamSource source = new FileInputStreamSource(metricFile, true)) {
+            testLog(metricFile.getName(), LogDataType.PB, source);
+        }
+    }
+
+    @Override
+    public void processMetricDirectory(String key, File metricDirectory, DeviceMetricData runData) {
+        // Implement if all the files under specific directory have to be post processed.
+    }
+
+    /**
+     * Run a host command with the given array of command args.
+     *
+     * @param commandArgs args to be used to construct the host command.
+     * @return return the command results.
+     */
+    @VisibleForTesting
+    protected CommandResult runHostCommand(String[] commandArgs) {
+        return RunUtil.getDefault().runTimedCmd(MAX_SCRIPT_TIMEOUT_MSECS, commandArgs);
+    }
+}
+
diff --git a/src/com/android/tradefed/invoker/InvocationExecution.java b/src/com/android/tradefed/invoker/InvocationExecution.java
index c226ab1..3c23f45 100644
--- a/src/com/android/tradefed/invoker/InvocationExecution.java
+++ b/src/com/android/tradefed/invoker/InvocationExecution.java
@@ -17,6 +17,7 @@
 
 import com.android.ddmlib.Log.LogLevel;
 import com.android.tradefed.build.BuildInfo;
+import com.android.tradefed.build.BuildInfoKey;
 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
 import com.android.tradefed.build.BuildRetrievalError;
 import com.android.tradefed.build.IBuildInfo;
@@ -119,6 +120,7 @@
                 }
                 // TODO: remove build update when reporting is done on context
                 updateBuild(info, config);
+                info.setTestResourceBuild(config.isDeviceConfiguredFake(currentDeviceName));
             }
         } catch (BuildRetrievalError e) {
             CLog.e(e);
@@ -130,6 +132,7 @@
             }
             throw e;
         }
+        createSharedResources(context);
         return true;
     }
 
@@ -645,6 +648,53 @@
         }
     }
 
+    /** Populate the shared resources directory for all non-resource build */
+    private void createSharedResources(IInvocationContext context) {
+        List<IBuildInfo> infos = context.getBuildInfos();
+        if (infos.size() <= 1) {
+            return;
+        }
+        try {
+            File resourcesDir = null;
+            for (IBuildInfo info : infos) {
+                if (info.isTestResourceBuild()) {
+                    if (resourcesDir == null) {
+                        resourcesDir = FileUtil.createTempDir("invocation-resources-dir");
+                    }
+                    // Create a reception sub-folder for each build info resource to avoid mixing
+                    String name =
+                            String.format(
+                                    "%s_%s_%s",
+                                    info.getBuildBranch(),
+                                    info.getBuildId(),
+                                    info.getBuildFlavor());
+                    File buildDir = FileUtil.createTempDir(name, resourcesDir);
+                    for (BuildInfoFileKey key : BuildInfoKey.SHARED_KEY) {
+                        File f = info.getFile(key);
+                        if (f == null) {
+                            continue;
+                        }
+                        File subDir = new File(buildDir, f.getName());
+                        FileUtil.symlinkFile(f, subDir);
+                    }
+                }
+            }
+            if (resourcesDir == null) {
+                return;
+            }
+            // Only set the shared dir on real build if it exists.
+            CLog.d("Creating shared resources directory.");
+            for (IBuildInfo info : infos) {
+                if (!info.isTestResourceBuild()) {
+                    info.setFile(BuildInfoFileKey.SHARED_RESOURCE_DIR, resourcesDir, "v1");
+                }
+            }
+        } catch (IOException e) {
+            CLog.e("Failed to create the shared resources dir.");
+            CLog.e(e);
+        }
+    }
+
     /** Returns the external directory coming from the environment. */
     @VisibleForTesting
     File getExternalTestCasesDirs(EnvVariable envVar) {
diff --git a/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java b/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
index 351fc88..8ffafe8 100644
--- a/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
+++ b/src/com/android/tradefed/postprocessor/AggregatePostProcessor.java
@@ -15,18 +15,17 @@
  */
 package com.android.tradefed.postprocessor;
 
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Measurements;
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.TestDescription;
 
 import com.google.common.collect.ArrayListMultimap;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -95,32 +94,8 @@
             if (rawValues.isEmpty()) {
                 continue;
             }
-            boolean areAllDoubles =
-                    rawValues
-                            .stream()
-                            .allMatch(
-                                    val -> {
-                                        try {
-                                            Double.parseDouble(val);
-                                            return true;
-                                        } catch (NumberFormatException e) {
-                                            return false;
-                                        }
-                                    });
-            if (areAllDoubles) {
-                List<Double> values =
-                        rawValues.stream().map(Double::parseDouble).collect(Collectors.toList());
-                HashMap<String, Double> stats = getStats(values);
-                for (String statKey : stats.keySet()) {
-                    Metric.Builder metricBuilder = Metric.newBuilder();
-                    metricBuilder
-                            .getMeasurementsBuilder()
-                            .setSingleString(String.format("%2.2f", stats.get(statKey)));
-                    aggregateMetrics.put(
-                            String.join(STATS_KEY_SEPARATOR, metricKey, statKey), metricBuilder);
-                }
-            } else {
-                CLog.i("Metric %s from test %s is not numeric", metricKey, fullTestName);
+            if (isAllDoubleValues(rawValues)) {
+                buildStats(metricKey, rawValues, aggregateMetrics);
             }
         }
         return aggregateMetrics;
@@ -128,7 +103,62 @@
 
     @Override
     public Map<String, Metric.Builder> processRunMetrics(HashMap<String, Metric> rawMetrics) {
-        return new HashMap<String, Metric.Builder>();
+        // Aggregate the test run metrics which has comma separated values which can be
+        // parsed to double values.
+        Map<String, Metric.Builder> aggregateMetrics = new HashMap<String, Metric.Builder>();
+        for (Map.Entry<String, Metric> entry : rawMetrics.entrySet()) {
+            String values = entry.getValue().getMeasurements().getSingleString();
+            List<String> splitVals = Arrays.asList(values.split(",", 0));
+            // Build stats only for the keys with more than one value.
+            if (isAllDoubleValues(splitVals) && splitVals.size() > 1) {
+                buildStats(entry.getKey(), splitVals, aggregateMetrics);
+            }
+        }
+        return aggregateMetrics;
+    }
+
+    /**
+     * Return true is all the values can be parsed to double value.
+     * Otherwise return false.
+     * @param rawValues list whose values are validated.
+     * @return
+     */
+    private boolean isAllDoubleValues(List<String> rawValues) {
+        return rawValues
+                .stream()
+                .allMatch(
+                        val -> {
+                            try {
+                                Double.parseDouble(val);
+                                return true;
+                            } catch (NumberFormatException e) {
+                                return false;
+                            }
+                        });
+    }
+
+    /**
+     * Build stats for the given set of values and build the metrics using the metric key
+     * and stats name and update the results in aggregated metrics.
+     *
+     * @param metricKey key to which the values correspond to.
+     * @param values list of raw values.
+     * @param aggregateMetrics where final metrics will be stored.
+     */
+    private void buildStats(String metricKey, List<String> values,
+            Map<String, Metric.Builder> aggregateMetrics) {
+        List<Double> doubleValues =
+                values.stream().map(Double::parseDouble).collect(Collectors.toList());
+        HashMap<String, Double> stats = getStats(doubleValues);
+        for (String statKey : stats.keySet()) {
+            Metric.Builder metricBuilder = Metric.newBuilder();
+            metricBuilder
+                    .getMeasurementsBuilder()
+                    .setSingleString(String.format("%2.2f", stats.get(statKey)));
+            aggregateMetrics.put(
+                    String.join(STATS_KEY_SEPARATOR, metricKey, statKey),
+                    metricBuilder);
+        }
     }
 
     private HashMap<String, Double> getStats(Collection<Double> values) {
diff --git a/src/com/android/tradefed/result/proto/FileProtoResultReporter.java b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
new file mode 100644
index 0000000..f72abf4
--- /dev/null
+++ b/src/com/android/tradefed/result/proto/FileProtoResultReporter.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.proto;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/** Proto reporter that dumps the {@link TestRecord} into a file. */
+public class FileProtoResultReporter extends ProtoResultReporter {
+
+    @Option(
+        name = "proto-output-file",
+        description = "File where the proto output will be saved",
+        mandatory = true
+    )
+    private File mOutputFile;
+
+    @Override
+    public void processFinalProto(TestRecord finalRecord) {
+        try {
+            finalRecord.writeDelimitedTo(new FileOutputStream(mOutputFile));
+        } catch (IOException e) {
+            CLog.e(e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    /** Sets the file where to output the result. */
+    public void setFileOutput(File output) {
+        mOutputFile = output;
+    }
+}
diff --git a/src/com/android/tradefed/suite/checker/ShellStatusChecker.java b/src/com/android/tradefed/suite/checker/ShellStatusChecker.java
new file mode 100644
index 0000000..313a923
--- /dev/null
+++ b/src/com/android/tradefed/suite/checker/ShellStatusChecker.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.suite.checker;
+
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
+
+/**
+ * Check if the shell status is as expected before and after a module run. Any changes may
+ * unexpectedly affect test cases.
+ *
+ * <p>There is a command-line option to disable the checker entirely:
+ *
+ * <pre>--skip-system-status-check=com.android.tradefed.suite.checker.ShellStatusChecker
+ * </pre>
+ */
+@OptionClass(alias = "shell-status-checker")
+public class ShellStatusChecker implements ISystemStatusChecker {
+
+    private static final String FAIL_STRING = " Reset failed.";
+
+    /* Option for the expected root state at pre- and post-check. */
+    @Option(
+        name = "expect-root",
+        description =
+                "Controls whether the shell root status is expected to be "
+                        + "root ('true') or non-root ('false'). "
+                        + "The checker will warn and try to revert the state "
+                        + "if not as expected at pre- or post-check."
+    )
+    private boolean mExpectedRoot = false;
+
+    /** {@inheritDoc} */
+    @Override
+    public StatusCheckerResult preExecutionCheck(ITestDevice device)
+            throws DeviceNotAvailableException {
+        StatusCheckerResult result = new StatusCheckerResult(CheckStatus.SUCCESS);
+        if (mExpectedRoot != device.isAdbRoot()) {
+            String message =
+                    "This module unexpectedly started in a "
+                            + (mExpectedRoot ? "non-root" : "root")
+                            + " shell. Leaked from earlier module?";
+            result.setStatus(CheckStatus.FAILED);
+
+            boolean reset;
+            if (mExpectedRoot) {
+                reset = device.enableAdbRoot();
+            } else {
+                reset = device.disableAdbRoot();
+            }
+            if (!reset) {
+                message += FAIL_STRING;
+            }
+            CLog.w(message);
+            result.setErrorMessage(message);
+        }
+
+        return result;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public StatusCheckerResult postExecutionCheck(ITestDevice device)
+            throws DeviceNotAvailableException {
+        StatusCheckerResult result = new StatusCheckerResult(CheckStatus.SUCCESS);
+        if (mExpectedRoot != device.isAdbRoot()) {
+            String message =
+                    "This module changed shell root status to "
+                            + (mExpectedRoot ? "non-root" : "root")
+                            + ". Leaked from a test case or setup?";
+            result.setStatus(CheckStatus.FAILED);
+
+            boolean reset;
+            if (mExpectedRoot) {
+                reset = device.enableAdbRoot();
+            } else {
+                reset = device.disableAdbRoot();
+            }
+            if (!reset) {
+                message += FAIL_STRING;
+            }
+            CLog.w(message);
+            result.setErrorMessage(message);
+        }
+
+        return result;
+    }
+}
diff --git a/src/com/android/tradefed/targetprep/AppSetup.java b/src/com/android/tradefed/targetprep/AppSetup.java
index d2aad34..494ce2d 100644
--- a/src/com/android/tradefed/targetprep/AppSetup.java
+++ b/src/com/android/tradefed/targetprep/AppSetup.java
@@ -61,10 +61,17 @@
             "force retention of this package when --uninstall-all is set.")
     private Set<String> mSkipUninstallPkgs = new HashSet<String>();
 
-    @Option(name = "install-flag", description =
-            "optional flag(s) to provide when installing apks.")
+    /** @deprecated Use "install-arg" instead. */
+    @Deprecated
+    @Option(
+        name = "install-flag",
+        description = "optional flag(s) to provide when installing apks."
+    )
     private ArrayList<String> mInstallFlags = new ArrayList<>();
 
+    @Option(name = "install-arg", description = "optional flag(s) to provide when installing apks.")
+    private ArrayList<String> mInstallArgs = new ArrayList<>();
+
     @Option(name = "post-install-cmd", description =
             "optional post-install adb shell commands; can be repeated.")
     private List<String> mPostInstallCmds = new ArrayList<>();
@@ -120,8 +127,11 @@
                         continue;
                     }
                 }
-                String result = device.installPackage(apkFile.getFile(), true,
-                        mInstallFlags.toArray(new String[mInstallFlags.size()]));
+                List<String> args = new ArrayList<>(mInstallArgs);
+                args.addAll(mInstallFlags);
+                String result =
+                        device.installPackage(
+                                apkFile.getFile(), true, args.toArray(new String[args.size()]));
                 if (result != null) {
                     // typically install failures means something is wrong with apk.
                     // TODO: in future add more logic to throw targetsetup vs build vs
diff --git a/src/com/android/tradefed/testtype/GTestXmlResultParser.java b/src/com/android/tradefed/testtype/GTestXmlResultParser.java
index 826ec87..2125f18 100644
--- a/src/com/android/tradefed/testtype/GTestXmlResultParser.java
+++ b/src/com/android/tradefed/testtype/GTestXmlResultParser.java
@@ -47,6 +47,7 @@
 
     private final static String TEST_SUITE_TAG = "testsuite";
     private final static String TEST_CASE_TAG = "testcase";
+    private static final String SKIPPED_VALUE = "skipped";
 
     private final String mTestRunName;
     private int mNumTestsRun = 0;
@@ -162,6 +163,7 @@
         String classname = testcase.getAttribute("classname");
         String testname = testcase.getAttribute("name");
         String runtime = testcase.getAttribute("time");
+        String status = testcase.getAttribute("status");
         ParsedTestInfo parsedResults = new ParsedTestInfo(testname, classname, runtime);
         TestDescription testId =
                 new TestDescription(parsedResults.mTestClassName, parsedResults.mTestName);
@@ -170,6 +172,11 @@
             listener.testStarted(testId);
         }
 
+        if (SKIPPED_VALUE.equals(status)) {
+            for (ITestInvocationListener listener : mTestListeners) {
+                listener.testIgnored(testId);
+            }
+        }
         // If there is a failure tag report failure
         if (testcase.getElementsByTagName("failure").getLength() != 0) {
             String trace = ((Element)testcase.getElementsByTagName("failure").item(0))
diff --git a/src/com/android/tradefed/testtype/TfTestLauncher.java b/src/com/android/tradefed/testtype/TfTestLauncher.java
index 4a35abb..22cb283 100644
--- a/src/com/android/tradefed/testtype/TfTestLauncher.java
+++ b/src/com/android/tradefed/testtype/TfTestLauncher.java
@@ -320,7 +320,7 @@
         runMetrics.put(
                 "elapsed-time", TfMetricProtoUtil.stringToMetric(Long.toString(elapsedTime)));
         listener.testEnded(tid, runMetrics);
-        listener.testRunEnded(elapsedTime, runMetrics);
+        listener.testRunEnded(0L, runMetrics);
     }
 
     /**
diff --git a/src/com/android/tradefed/testtype/metricregression/DetectRegression.java b/src/com/android/tradefed/testtype/metricregression/DetectRegression.java
deleted file mode 100644
index 9f4123b..0000000
--- a/src/com/android/tradefed/testtype/metricregression/DetectRegression.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.metricregression;
-
-import com.android.ddmlib.Log;
-import com.android.tradefed.config.Option;
-import com.android.tradefed.config.OptionClass;
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.ITestInvocationListener;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.testtype.IRemoteTest;
-import com.android.tradefed.testtype.suite.ModuleDefinition;
-import com.android.tradefed.util.FileUtil;
-import com.android.tradefed.util.MetricsXmlParser;
-import com.android.tradefed.util.MetricsXmlParser.ParseException;
-import com.android.tradefed.util.MultiMap;
-import com.android.tradefed.util.Pair;
-import com.android.tradefed.util.TableBuilder;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import com.google.common.primitives.Doubles;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Random;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/** An algorithm to detect local metrics regression. */
-@OptionClass(alias = "regression")
-public class DetectRegression implements IRemoteTest {
-
-    @Option(
-        name = "pre-patch-metrics",
-        description = "Path to pre-patch metrics folder.",
-        mandatory = true
-    )
-    private File mPrePatchFolder;
-
-    @Option(
-        name = "post-patch-metrics",
-        description = "Path to post-patch metrics folder.",
-        mandatory = true
-    )
-    private File mPostPatchFolder;
-
-    @Option(
-        name = "strict-mode",
-        description = "When before/after metrics mismatch, true=throw exception, false=log error"
-    )
-    private boolean mStrict = false;
-
-    @Option(name = "blacklist-metrics", description = "Ignore metrics that match these names")
-    private Set<String> mBlacklistMetrics = new HashSet<>();
-
-    private static final String TITLE = "Metric Regressions";
-    private static final String PROLOG =
-            "\n====================Metrics Comparison Results====================\nTest Summary\n";
-    private static final String EPILOG =
-            "==================End Metrics Comparison Results==================\n";
-    private static final String[] TABLE_HEADER = {
-        "Metric Name", "Pre Avg", "Post Avg", "False Positive Probability"
-    };
-    /** Matches metrics xml filenames. */
-    private static final String METRICS_PATTERN = "metrics-.*\\.xml";
-
-    private static final int SAMPLES = 100000;
-    private static final double STD_DEV_THRESHOLD = 2.0;
-
-    private static final Set<String> DEFAULT_IGNORE =
-            ImmutableSet.of(
-                    ModuleDefinition.PREPARATION_TIME,
-                    ModuleDefinition.TEST_TIME,
-                    ModuleDefinition.TEAR_DOWN_TIME);
-
-    @VisibleForTesting
-    public static class TableRow {
-        String name;
-        double preAvg;
-        double postAvg;
-        double probability;
-
-        public String[] toStringArray() {
-            return new String[] {
-                name,
-                String.format("%.2f", preAvg),
-                String.format("%.2f", postAvg),
-                String.format("%.3f", probability)
-            };
-        }
-    }
-
-    public DetectRegression() {
-        mBlacklistMetrics.addAll(DEFAULT_IGNORE);
-    }
-
-    @Override
-    public void run(ITestInvocationListener listener) {
-        try {
-            // Load metrics from files, and validate them.
-            Metrics before =
-                    MetricsXmlParser.parse(
-                            mBlacklistMetrics, mStrict, getMetricsFiles(mPrePatchFolder));
-            Metrics after =
-                    MetricsXmlParser.parse(
-                            mBlacklistMetrics, mStrict, getMetricsFiles(mPostPatchFolder));
-            before.crossValidate(after);
-            runRegressionDetection(before, after);
-        } catch (IOException | ParseException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    /**
-     * Computes metrics regression between pre-patch and post-patch.
-     *
-     * @param before pre-patch metrics
-     * @param after post-patch metrics
-     */
-    @VisibleForTesting
-    void runRegressionDetection(Metrics before, Metrics after) {
-        Set<String> runMetricsToCompare =
-                Sets.intersection(before.getRunMetrics().keySet(), after.getRunMetrics().keySet());
-        List<TableRow> runMetricsResult = new ArrayList<>();
-        for (String name : runMetricsToCompare) {
-            List<Double> beforeMetrics = before.getRunMetrics().get(name);
-            List<Double> afterMetrics = after.getRunMetrics().get(name);
-            if (computeRegression(beforeMetrics, afterMetrics)) {
-                runMetricsResult.add(getTableRow(name, beforeMetrics, afterMetrics));
-            }
-        }
-
-        Set<Pair<TestDescription, String>> testMetricsToCompare =
-                Sets.intersection(
-                        before.getTestMetrics().keySet(), after.getTestMetrics().keySet());
-        MultiMap<String, TableRow> testMetricsResult = new MultiMap<>();
-        for (Pair<TestDescription, String> id : testMetricsToCompare) {
-            List<Double> beforeMetrics = before.getTestMetrics().get(id);
-            List<Double> afterMetrics = after.getTestMetrics().get(id);
-            if (computeRegression(beforeMetrics, afterMetrics)) {
-                testMetricsResult.put(
-                        id.first.toString(), getTableRow(id.second, beforeMetrics, afterMetrics));
-            }
-        }
-        logResult(before, after, runMetricsResult, testMetricsResult);
-    }
-
-    /** Prints results to the console. */
-    @VisibleForTesting
-    void logResult(
-            Metrics before,
-            Metrics after,
-            List<TableRow> runMetricsResult,
-            MultiMap<String, TableRow> testMetricsResult) {
-        TableBuilder table = new TableBuilder(TABLE_HEADER.length);
-        table.addTitle(TITLE).addLine(TABLE_HEADER).addDoubleLineSeparator();
-
-        int totalRunMetrics =
-                Sets.intersection(before.getRunMetrics().keySet(), after.getRunMetrics().keySet())
-                        .size();
-        String runResult =
-                String.format(
-                        "Run Metrics (%d compared, %d changed)",
-                        totalRunMetrics, runMetricsResult.size());
-        table.addLine(runResult).addSingleLineSeparator();
-        runMetricsResult.stream().map(TableRow::toStringArray).forEach(table::addLine);
-        if (!runMetricsResult.isEmpty()) {
-            table.addSingleLineSeparator();
-        }
-
-        int totalTestMetrics =
-                Sets.intersection(before.getTestMetrics().keySet(), after.getTestMetrics().keySet())
-                        .size();
-        int changedTestMetrics =
-                testMetricsResult
-                        .keySet()
-                        .stream()
-                        .mapToInt(k -> testMetricsResult.get(k).size())
-                        .sum();
-        String testResult =
-                String.format(
-                        "Test Metrics (%d compared, %d changed)",
-                        totalTestMetrics, changedTestMetrics);
-        table.addLine(testResult).addSingleLineSeparator();
-        for (String test : testMetricsResult.keySet()) {
-            table.addLine("> " + test);
-            testMetricsResult
-                    .get(test)
-                    .stream()
-                    .map(TableRow::toStringArray)
-                    .forEach(table::addLine);
-            table.addBlankLineSeparator();
-        }
-        table.addDoubleLineSeparator();
-
-        StringBuilder sb = new StringBuilder(PROLOG);
-        sb.append(
-                String.format(
-                        "%d tests. %d sets of pre-patch metrics. %d sets of post-patch metrics.\n\n",
-                        before.getNumTests(), before.getNumRuns(), after.getNumRuns()));
-        sb.append(table.build()).append('\n').append(EPILOG);
-
-        CLog.logAndDisplay(Log.LogLevel.INFO, sb.toString());
-    }
-
-    private List<File> getMetricsFiles(File folder) throws IOException {
-        CLog.i("Loading metrics from: %s", mPrePatchFolder.getAbsolutePath());
-        return FileUtil.findFiles(folder, METRICS_PATTERN)
-                .stream()
-                .map(File::new)
-                .collect(Collectors.toList());
-    }
-
-    private static TableRow getTableRow(String name, List<Double> before, List<Double> after) {
-        TableRow row = new TableRow();
-        row.name = name;
-        row.preAvg = calcMean(before);
-        row.postAvg = calcMean(after);
-        row.probability = probFalsePositive(before.size(), after.size());
-        return row;
-    }
-
-    /** @return true if there is regression from before to after, false otherwise */
-    @VisibleForTesting
-    static boolean computeRegression(List<Double> before, List<Double> after) {
-        final double mean = calcMean(before);
-        final double stdDev = calcStdDev(before);
-        int regCount = 0;
-        for (double value : after) {
-            if (Math.abs(value - mean) > stdDev * STD_DEV_THRESHOLD) {
-                regCount++;
-            }
-        }
-        return regCount > after.size() / 2;
-    }
-
-    @VisibleForTesting
-    static double calcMean(List<Double> list) {
-        return list.stream().collect(Collectors.averagingDouble(x -> x));
-    }
-
-    @VisibleForTesting
-    static double calcStdDev(List<Double> list) {
-        final double mean = calcMean(list);
-        return Math.sqrt(
-                list.stream().collect(Collectors.averagingDouble(x -> Math.pow(x - mean, 2))));
-    }
-
-    private static double probFalsePositive(int priorRuns, int postRuns) {
-        int failures = 0;
-        Random rand = new Random();
-        for (int run = 0; run < SAMPLES; run++) {
-            double[] prior = new double[priorRuns];
-            for (int x = 0; x < priorRuns; x++) {
-                prior[x] = rand.nextGaussian();
-            }
-            double estMu = calcMean(Doubles.asList(prior));
-            double estStd = calcStdDev(Doubles.asList(prior));
-            int count = 0;
-            for (int y = 0; y < postRuns; y++) {
-                if (Math.abs(rand.nextGaussian() - estMu) > estStd * STD_DEV_THRESHOLD) {
-                    count++;
-                }
-            }
-            failures += count > postRuns / 2 ? 1 : 0;
-        }
-        return (double) failures / SAMPLES;
-    }
-}
diff --git a/src/com/android/tradefed/testtype/metricregression/Metrics.java b/src/com/android/tradefed/testtype/metricregression/Metrics.java
deleted file mode 100644
index 251d477..0000000
--- a/src/com/android/tradefed/testtype/metricregression/Metrics.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.metricregression;
-
-import com.android.tradefed.log.LogUtil.CLog;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.util.MetricsXmlParser;
-import com.android.tradefed.util.MultiMap;
-import com.android.tradefed.util.Pair;
-
-import com.google.common.annotations.VisibleForTesting;
-
-/** A metrics object to hold run metrics and test metrics parsed by {@link MetricsXmlParser} */
-public class Metrics {
-    private int mNumRuns;
-    private int mNumTests = -1;
-    private final boolean mStrictMode;
-    private final MultiMap<String, Double> mRunMetrics = new MultiMap<>();
-    private final MultiMap<Pair<TestDescription, String>, Double> mTestMetrics = new MultiMap<>();
-
-    /** Throw when metrics validation fails in strict mode. */
-    public static class MetricsException extends RuntimeException {
-        MetricsException(String cause) {
-            super(cause);
-        }
-    }
-
-    /**
-     * Constructs an empty Metrics object.
-     *
-     * @param strictMode whether exception should be thrown when validation fails
-     */
-    public Metrics(boolean strictMode) {
-        mStrictMode = strictMode;
-    }
-
-    /**
-     * Sets the number of tests. This method also checks if each call sets the same number of test,
-     * since this number should be consistent across multiple runs.
-     *
-     * @param numTests the number of tests
-     * @throws MetricsException if subsequent calls set a different number.
-     */
-    public void setNumTests(int numTests) {
-        if (mNumTests == -1) {
-            mNumTests = numTests;
-        } else {
-            if (mNumTests != numTests) {
-                String msg =
-                        String.format(
-                                "Number of test entries differ: expect #%d actual #%d",
-                                mNumTests, numTests);
-                throw new MetricsException(msg);
-            }
-        }
-    }
-
-    /**
-     * Adds a run metric.
-     *
-     * @param name metric name
-     * @param value metric value
-     */
-    public void addRunMetric(String name, String value) {
-        try {
-            mRunMetrics.put(name, Double.parseDouble(value));
-        } catch (NumberFormatException e) {
-            // This is normal. We often get some string metrics like device name. Just log it.
-            CLog.w(String.format("Run metric \"%s\" is not a number: \"%s\"", name, value));
-        }
-    }
-
-    /**
-     * Adds a test metric.
-     *
-     * @param testId TestDescription of the metric
-     * @param name metric name
-     * @param value metric value
-     */
-    public void addTestMetric(TestDescription testId, String name, String value) {
-        Pair<TestDescription, String> metricId = new Pair<>(testId, name);
-        try {
-            mTestMetrics.put(metricId, Double.parseDouble(value));
-        } catch (NumberFormatException e) {
-            // This is normal. We often get some string metrics like device name. Just log it.
-            CLog.w(
-                    String.format(
-                            "Test %s metric \"%s\" is not a number: \"%s\"", testId, name, value));
-        }
-    }
-
-    /**
-     * Validates that the number of entries of each metric equals to the number of runs.
-     *
-     * @param numRuns number of runs
-     * @throws MetricsException when validation fails in strict mode
-     */
-    public void validate(int numRuns) {
-        mNumRuns = numRuns;
-        for (String name : mRunMetrics.keySet()) {
-            if (mRunMetrics.get(name).size() < mNumRuns) {
-                error(
-                        String.format(
-                                "Run metric \"%s\" too few entries: expected #%d actual #%d",
-                                name, mNumRuns, mRunMetrics.get(name).size()));
-            }
-        }
-        for (Pair<TestDescription, String> id : mTestMetrics.keySet()) {
-            if (mTestMetrics.get(id).size() < mNumRuns) {
-                error(
-                        String.format(
-                                "Test %s metric \"%s\" too few entries: expected #%d actual #%d",
-                                id.first, id.second, mNumRuns, mTestMetrics.get(id).size()));
-            }
-        }
-    }
-
-    /**
-     * Validates with after-patch Metrics object. Make sure two metrics object contain same run
-     * metric entries and test metric entries. Assume this object contains before-patch metrics.
-     *
-     * @param after a Metrics object containing after-patch metrics
-     * @throws MetricsException when cross validation fails in strict mode
-     */
-    public void crossValidate(Metrics after) {
-        if (mNumTests != after.mNumTests) {
-            error(
-                    String.format(
-                            "Number of test entries differ: before #%d after #%d",
-                            mNumTests, after.mNumTests));
-        }
-
-        for (String name : mRunMetrics.keySet()) {
-            if (!after.mRunMetrics.containsKey(name)) {
-                warn(String.format("Run metric \"%s\" only in before-patch run.", name));
-            }
-        }
-
-        for (String name : after.mRunMetrics.keySet()) {
-            if (!mRunMetrics.containsKey(name)) {
-                warn(String.format("Run metric \"%s\" only in after-patch run.", name));
-            }
-        }
-
-        for (Pair<TestDescription, String> id : mTestMetrics.keySet()) {
-            if (!after.mTestMetrics.containsKey(id)) {
-                warn(
-                        String.format(
-                                "Test %s metric \"%s\" only in before-patch run.",
-                                id.first, id.second));
-            }
-        }
-
-        for (Pair<TestDescription, String> id : after.mTestMetrics.keySet()) {
-            if (!mTestMetrics.containsKey(id)) {
-                warn(
-                        String.format(
-                                "Test %s metric \"%s\" only in after-patch run.",
-                                id.first, id.second));
-            }
-        }
-    }
-
-    @VisibleForTesting
-    void error(String msg) {
-        if (mStrictMode) {
-            throw new MetricsException(msg);
-        } else {
-            CLog.e(msg);
-        }
-    }
-
-    @VisibleForTesting
-    void warn(String msg) {
-        if (mStrictMode) {
-            throw new MetricsException(msg);
-        } else {
-            CLog.w(msg);
-        }
-    }
-
-    /**
-     * Gets the number of test runs stored in this object.
-     *
-     * @return number of test runs
-     */
-    public int getNumRuns() {
-        return mNumRuns;
-    }
-
-    /**
-     * Gets the number of tests stored in this object.
-     *
-     * @return number of tests
-     */
-    public int getNumTests() {
-        return mNumTests;
-    }
-
-    /**
-     * Gets all run metrics stored in this object.
-     *
-     * @return a {@link MultiMap} from test name String to Double
-     */
-    public MultiMap<String, Double> getRunMetrics() {
-        return mRunMetrics;
-    }
-
-    /**
-     * Gets all test metrics stored in this object.
-     *
-     * @return a {@link MultiMap} from (TestDescription, test name) pair to Double
-     */
-    public MultiMap<Pair<TestDescription, String>, Double> getTestMetrics() {
-        return mTestMetrics;
-    }
-}
diff --git a/src/com/android/tradefed/util/MetricsXmlParser.java b/src/com/android/tradefed/util/MetricsXmlParser.java
deleted file mode 100644
index 69084cc..0000000
--- a/src/com/android/tradefed/util/MetricsXmlParser.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.util;
-
-import com.android.tradefed.result.MetricsXMLResultReporter;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.testtype.metricregression.Metrics;
-
-import com.google.common.annotations.VisibleForTesting;
-
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-import java.util.Set;
-
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
-
-/** Parser that extracts test metrics result data generated by {@link MetricsXMLResultReporter}. */
-public class MetricsXmlParser {
-
-    /** Thrown when MetricsXmlParser fails to parse a metrics xml file. */
-    public static class ParseException extends Exception {
-        public ParseException(Throwable cause) {
-            super(cause);
-        }
-
-        public ParseException(String msg, Throwable cause) {
-            super(msg, cause);
-        }
-    }
-
-    /*
-     * Parses the xml format. Expected tags/attributes are:
-     * testsuite name="runname" tests="X"
-     *   runmetric name="metric1" value="1.0"
-     *   testcase classname="FooTest" testname="testMethodName"
-     *     testmetric name="metric2" value="1.0"
-     */
-    private static class MetricsXmlHandler extends DefaultHandler {
-
-        private static final String TESTSUITE_TAG = "testsuite";
-        private static final String TESTCASE_TAG = "testcase";
-        private static final String TIME_TAG = "time";
-        private static final String RUNMETRIC_TAG = "runmetric";
-        private static final String TESTMETRIC_TAG = "testmetric";
-
-        private TestDescription mCurrentTest = null;
-
-        private Metrics mMetrics;
-        private Set<String> mBlacklistMetrics;
-
-        public MetricsXmlHandler(Metrics metrics, Set<String> blacklistMetrics) {
-            mMetrics = metrics;
-            mBlacklistMetrics = blacklistMetrics;
-        }
-
-        @Override
-        public void startElement(String uri, String localName, String name, Attributes attributes)
-                throws SAXException {
-            if (TESTSUITE_TAG.equalsIgnoreCase(name)) {
-                // top level tag - maps to a test run in TF terminology
-                String testCount = getMandatoryAttribute(name, "tests", attributes);
-                mMetrics.setNumTests(Integer.parseInt(testCount));
-                mMetrics.addRunMetric(TIME_TAG, getMandatoryAttribute(name, TIME_TAG, attributes));
-            }
-            if (TESTCASE_TAG.equalsIgnoreCase(name)) {
-                // start of description of an individual test method
-                String testClassName = getMandatoryAttribute(name, "classname", attributes);
-                String methodName = getMandatoryAttribute(name, "testname", attributes);
-                mCurrentTest = new TestDescription(testClassName, methodName);
-            }
-            if (RUNMETRIC_TAG.equalsIgnoreCase(name)) {
-                String metricName = getMandatoryAttribute(name, "name", attributes);
-                String metricValue = getMandatoryAttribute(name, "value", attributes);
-                if (!mBlacklistMetrics.contains(metricName)) {
-                    mMetrics.addRunMetric(metricName, metricValue);
-                }
-            }
-            if (TESTMETRIC_TAG.equalsIgnoreCase(name)) {
-                String metricName = getMandatoryAttribute(name, "name", attributes);
-                String metricValue = getMandatoryAttribute(name, "value", attributes);
-                if (!mBlacklistMetrics.contains(metricName)) {
-                    mMetrics.addTestMetric(mCurrentTest, metricName, metricValue);
-                }
-            }
-        }
-
-        private String getMandatoryAttribute(String tagName, String attrName, Attributes attributes)
-                throws SAXException {
-            String value = attributes.getValue(attrName);
-            if (value == null) {
-                throw new SAXException(
-                        String.format(
-                                "Malformed XML, could not find '%s' attribute in '%s'",
-                                attrName, tagName));
-            }
-            return value;
-        }
-    }
-
-    /**
-     * Parses xml data contained in given input files.
-     *
-     * @param blacklistMetrics ignore the metrics with these names
-     * @param strictMode whether to throw an exception when metric validation fails
-     * @param metricXmlFiles a list of metric xml files
-     * @return a Metric object containing metrics from all metric files
-     * @throws ParseException if input could not be parsed
-     */
-    public static Metrics parse(
-            Set<String> blacklistMetrics, boolean strictMode, List<File> metricXmlFiles)
-            throws ParseException {
-        Metrics metrics = new Metrics(strictMode);
-        for (File xml : metricXmlFiles) {
-            try (InputStream is = new BufferedInputStream(new FileInputStream(xml))) {
-                parse(metrics, blacklistMetrics, is);
-            } catch (Exception e) {
-                throw new ParseException("Unable to parse " + xml.getPath(), e);
-            }
-        }
-        metrics.validate(metricXmlFiles.size());
-        return metrics;
-    }
-
-    @VisibleForTesting
-    public static Metrics parse(Metrics metrics, Set<String> blacklistMetrics, InputStream is)
-            throws ParseException {
-        try {
-            SAXParserFactory parserFactory = SAXParserFactory.newInstance();
-            parserFactory.setNamespaceAware(true);
-            SAXParser parser = parserFactory.newSAXParser();
-            parser.parse(is, new MetricsXmlHandler(metrics, blacklistMetrics));
-            return metrics;
-        } catch (ParserConfigurationException | SAXException | IOException e) {
-            throw new ParseException(e);
-        }
-    }
-}
diff --git a/src/com/android/tradefed/util/SubprocessTestResultsParser.java b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
index 0a93e98..09d908d 100644
--- a/src/com/android/tradefed/util/SubprocessTestResultsParser.java
+++ b/src/com/android/tradefed/util/SubprocessTestResultsParser.java
@@ -102,6 +102,7 @@
     private class EventReceiverThread extends Thread {
         private ServerSocket mSocket;
         private CountDownLatch mCountDown;
+        private boolean mShouldParse = true;
 
         public EventReceiverThread() throws IOException {
             super("EventReceiverThread");
@@ -123,6 +124,14 @@
             }
         }
 
+        /**
+         * When reaching some issues, we might want to terminate the buffer of the socket to spy
+         * which events are still in the pipe.
+         */
+        public void stopParsing() {
+            mShouldParse = false;
+        }
+
         @Override
         public void run() {
             Socket client = null;
@@ -133,8 +142,12 @@
                 String event = null;
                 while ((event = in.readLine()) != null) {
                     try {
-                        CLog.d("received event: '%s'", event);
-                        parse(event);
+                        if (mShouldParse) {
+                            CLog.d("received event: '%s'", event);
+                            parse(event);
+                        } else {
+                            CLog.d("Skipping parsing of event: '%s'", event);
+                        }
                     } catch (JSONException e) {
                         CLog.e(e);
                     }
@@ -151,6 +164,7 @@
 
     /**
      * If the event receiver is being used, ensure that we wait for it to terminate.
+     *
      * @param millis timeout in milliseconds.
      * @return True if receiver thread terminate before timeout, False otherwise.
      */
@@ -159,6 +173,7 @@
             try {
                 CLog.i("Waiting for events to finish being processed.");
                 if (!mEventReceiver.getCountDown().await(millis, TimeUnit.MILLISECONDS)) {
+                    mEventReceiver.stopParsing();
                     CLog.e("Event receiver thread did not complete. Some events may be missing.");
                     return false;
                 }
diff --git a/src/com/android/tradefed/util/ZipUtil2.java b/src/com/android/tradefed/util/ZipUtil2.java
index d0b739c..b14ba7d 100644
--- a/src/com/android/tradefed/util/ZipUtil2.java
+++ b/src/com/android/tradefed/util/ZipUtil2.java
@@ -24,6 +24,8 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * A helper class for zip extraction that takes POSIX file permissions into account
@@ -33,21 +35,20 @@
     /**
      * A util method to apply unix mode from {@link ZipArchiveEntry} to the created local file
      * system entry if necessary
+     *
      * @param entry the entry inside zipfile (potentially contains mode info)
      * @param localFile the extracted local file entry
+     * @return True if the Unix permissions are set, false otherwise.
      * @throws IOException
      */
-    private static void applyUnixModeIfNecessary(ZipArchiveEntry entry, File localFile)
+    private static boolean applyUnixModeIfNecessary(ZipArchiveEntry entry, File localFile)
             throws IOException {
         if (entry.getPlatform() == ZipArchiveEntry.PLATFORM_UNIX) {
             Files.setPosixFilePermissions(localFile.toPath(),
                     FileUtil.unixModeToPosix(entry.getUnixMode()));
-        } else {
-            CLog.d(
-                    "Entry '%s' exists but does not contain Unix mode permission info. File will "
-                            + "have default permission.",
-                    entry.getName());
+            return true;
         }
+        return false;
     }
 
     /**
@@ -59,19 +60,30 @@
      */
     public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
         Enumeration<? extends ZipArchiveEntry> entries = zipFile.getEntries();
+        Set<String> noPermissions = new HashSet<>();
         while (entries.hasMoreElements()) {
             ZipArchiveEntry entry = entries.nextElement();
             File childFile = new File(destDir, entry.getName());
             childFile.getParentFile().mkdirs();
             if (entry.isDirectory()) {
                 childFile.mkdirs();
-                applyUnixModeIfNecessary(entry, childFile);
+                if (!applyUnixModeIfNecessary(entry, childFile)) {
+                    noPermissions.add(entry.getName());
+                }
                 continue;
             } else {
                 FileUtil.writeToFile(zipFile.getInputStream(entry), childFile);
-                applyUnixModeIfNecessary(entry, childFile);
+                if (!applyUnixModeIfNecessary(entry, childFile)) {
+                    noPermissions.add(entry.getName());
+                }
             }
         }
+        if (!noPermissions.isEmpty()) {
+            CLog.d(
+                    "Entries '%s' exist but do not contain Unix mode permission info. Files will "
+                            + "have default permission.",
+                    noPermissions);
+        }
     }
 
     /**
diff --git a/tests/res/testtype/gtest_output6.xml b/tests/res/testtype/gtest_output6.xml
new file mode 100644
index 0000000..3d3c229
--- /dev/null
+++ b/tests/res/testtype/gtest_output6.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuites tests="6" failures="0" disabled="0" errors="0" timestamp="2016-03-02T13:48:44" time="0.004" name="AllTests">
+  <testsuite name="InteropTest" tests="3" failures="0" disabled="0" errors="0" time="0.003">
+    <testcase name="test_lookup_hit" status="run" time="0.001" classname="InteropTest" />
+    <testcase name="test_lookup_miss" status="skipped" time="0.001" classname="InteropTest" />
+    <testcase name="test_dynamic" status="run" time="0.001" classname="InteropTest" />
+  </testsuite>
+  <testsuite name="ClassicPeerTest" tests="3" failures="0" disabled="0" errors="0" time="0.001">
+    <testcase name="test_basic_get" status="run" time="0" classname="ClassicPeerTest" />
+    <testcase name="test_multi_get_are_same" status="run" time="0" classname="ClassicPeerTest" />
+    <testcase name="test_multi_get_different" status="run" time="0" classname="ClassicPeerTest" />
+  </testsuite>
+</testsuites>
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index d99d0b2..d48c48a 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -74,6 +74,7 @@
 import com.android.tradefed.device.cloud.GceRemoteCmdFormatterTest;
 import com.android.tradefed.device.cloud.GceSshTunnelMonitorTest;
 import com.android.tradefed.device.cloud.RemoteFileUtilTest;
+import com.android.tradefed.device.helper.TelephonyHelperTest;
 import com.android.tradefed.device.metric.AtraceCollectorTest;
 import com.android.tradefed.device.metric.AtraceRunMetricCollectorTest;
 import com.android.tradefed.device.metric.BaseDeviceMetricCollectorTest;
@@ -87,6 +88,7 @@
 import com.android.tradefed.device.metric.IonHeapInfoMetricCollectorTest;
 import com.android.tradefed.device.metric.MemInfoMetricCollectorTest;
 import com.android.tradefed.device.metric.PagetypeInfoMetricCollectorTest;
+import com.android.tradefed.device.metric.PerfettoPullerMetricCollectorTest;
 import com.android.tradefed.device.metric.ProcessMaxMemoryCollectorTest;
 import com.android.tradefed.device.metric.ScheduleMultipleDeviceMetricCollectorTest;
 import com.android.tradefed.device.metric.ScheduledDeviceMetricCollectorTest;
@@ -140,6 +142,7 @@
 import com.android.tradefed.result.TestRunResultTest;
 import com.android.tradefed.result.TestSummaryTest;
 import com.android.tradefed.result.XmlResultReporterTest;
+import com.android.tradefed.result.proto.FileProtoResultReporterTest;
 import com.android.tradefed.result.proto.ProtoResultParserTest;
 import com.android.tradefed.result.proto.ProtoResultReporterTest;
 import com.android.tradefed.result.proto.StreamProtoResultReporterTest;
@@ -153,6 +156,7 @@
 import com.android.tradefed.suite.checker.DeviceSettingCheckerTest;
 import com.android.tradefed.suite.checker.KeyguardStatusCheckerTest;
 import com.android.tradefed.suite.checker.LeakedThreadStatusCheckerTest;
+import com.android.tradefed.suite.checker.ShellStatusCheckerTest;
 import com.android.tradefed.suite.checker.SystemServerFileDescriptorCheckerTest;
 import com.android.tradefed.suite.checker.SystemServerStatusCheckerTest;
 import com.android.tradefed.suite.checker.TimeStatusCheckerTest;
@@ -221,8 +225,6 @@
 import com.android.tradefed.testtype.junit4.BaseHostJUnit4TestTest;
 import com.android.tradefed.testtype.junit4.DeviceParameterizedRunnerTest;
 import com.android.tradefed.testtype.junit4.LongevityHostRunnerTest;
-import com.android.tradefed.testtype.metricregression.DetectRegressionTest;
-import com.android.tradefed.testtype.metricregression.MetricsTest;
 import com.android.tradefed.testtype.python.PythonBinaryHostTestTest;
 import com.android.tradefed.testtype.suite.AtestRunnerTest;
 import com.android.tradefed.testtype.suite.BaseTestSuiteTest;
@@ -274,7 +276,6 @@
 import com.android.tradefed.util.ListInstrumentationParserTest;
 import com.android.tradefed.util.LocalRunInstructionBuilderTest;
 import com.android.tradefed.util.LogcatEventParserTest;
-import com.android.tradefed.util.MetricsXmlParserTest;
 import com.android.tradefed.util.MultiMapTest;
 import com.android.tradefed.util.NullUtilTest;
 import com.android.tradefed.util.PairTest;
@@ -405,6 +406,9 @@
     RemoteAndroidDeviceTest.class,
     RemoteFileUtilTest.class,
 
+    // device.helper
+    TelephonyHelperTest.class,
+
     // device.metric
     AtraceCollectorTest.class,
     AtraceRunMetricCollectorTest.class,
@@ -419,6 +423,7 @@
     IonHeapInfoMetricCollectorTest.class,
     MemInfoMetricCollectorTest.class,
     PagetypeInfoMetricCollectorTest.class,
+    PerfettoPullerMetricCollectorTest.class,
     ProcessMaxMemoryCollectorTest.class,
     ScheduledDeviceMetricCollectorTest.class,
     ScheduleMultipleDeviceMetricCollectorTest.class,
@@ -498,6 +503,7 @@
     XmlResultReporterTest.class,
 
     // result.proto
+    FileProtoResultReporterTest.class,
     ProtoResultParserTest.class,
     ProtoResultReporterTest.class,
     StreamProtoResultReporterTest.class,
@@ -557,6 +563,7 @@
     DeviceSettingCheckerTest.class,
     KeyguardStatusCheckerTest.class,
     LeakedThreadStatusCheckerTest.class,
+    ShellStatusCheckerTest.class,
     SystemServerFileDescriptorCheckerTest.class,
     SystemServerStatusCheckerTest.class,
     TimeStatusCheckerTest.class,
@@ -598,10 +605,6 @@
     DeviceParameterizedRunnerTest.class,
     LongevityHostRunnerTest.class,
 
-    // testtype/metricregression
-    DetectRegressionTest.class,
-    MetricsTest.class,
-
     // testtype/python
     PythonBinaryHostTestTest.class,
 
@@ -666,7 +669,6 @@
     LegacySubprocessResultsReporterTest.class,
     ListInstrumentationParserTest.class,
     LogcatEventParserTest.class,
-    MetricsXmlParserTest.class,
     MultiMapTest.class,
     NullUtilTest.class,
     PairTest.class,
diff --git a/tests/src/com/android/tradefed/build/FileDownloadCacheFuncTest.java b/tests/src/com/android/tradefed/build/FileDownloadCacheFuncTest.java
index 34d1ebb..db58e1b 100644
--- a/tests/src/com/android/tradefed/build/FileDownloadCacheFuncTest.java
+++ b/tests/src/com/android/tradefed/build/FileDownloadCacheFuncTest.java
@@ -15,15 +15,23 @@
  */
 package com.android.tradefed.build;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
 import com.android.ddmlib.Log;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.StreamUtil;
 
-import junit.framework.TestCase;
-
 import org.easymock.EasyMock;
 import org.easymock.IAnswer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
@@ -34,10 +42,9 @@
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-/**
- * Longer running, concurrency based tests for {@link FileDownloadCache}.
- */
-public class FileDownloadCacheFuncTest extends TestCase {
+/** Longer running, concurrency based tests for {@link FileDownloadCache}. */
+@RunWith(JUnit4.class)
+public class FileDownloadCacheFuncTest {
 
     private static final String REMOTE_PATH = "path";
     private static final String DOWNLOADED_CONTENTS = "downloaded contents";
@@ -49,29 +56,27 @@
     private File mTmpDir;
     private List<File> mReturnedFiles;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
         mMockDownloader = EasyMock.createStrictMock(IFileDownloader.class);
         mTmpDir = FileUtil.createTempDir("functest");
         mCache = new FileDownloadCache(mTmpDir);
         mReturnedFiles = new ArrayList<File>(2);
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         for (File file : mReturnedFiles) {
             file.delete();
         }
         FileUtil.recursiveDelete(mTmpDir);
-        super.tearDown();
     }
 
     /**
      * Test {@link FileDownloadCache#fetchRemoteFile(IFileDownloader, String)} being called
      * concurrently by two separate threads.
      */
-    @SuppressWarnings("unchecked")
+    @Test
     public void testFetchRemoteFile_concurrent() throws Exception {
         // Simulate a relatively slow file download
         IAnswer<Object> slowDownloadAnswer = new IAnswer<Object>() {
@@ -87,6 +92,9 @@
         // is done, then link the downloaded file.
         mMockDownloader.downloadFile(EasyMock.eq(REMOTE_PATH), EasyMock.<File>anyObject());
         EasyMock.expectLastCall().andAnswer(slowDownloadAnswer);
+        EasyMock.expect(mMockDownloader.isFresh(EasyMock.anyObject(), EasyMock.eq(REMOTE_PATH)))
+                .andReturn(true);
+
         EasyMock.replay(mMockDownloader);
         Thread downloadThread1 = createDownloadThread(mMockDownloader, REMOTE_PATH);
         downloadThread1.setName("FileDownloadCacheFuncTest#testFetchRemoteFile_concurrent-1");
@@ -111,6 +119,7 @@
      * Test {@link FileDownloadCache#fetchRemoteFile(IFileDownloader, String)} being called
      * concurrently by multiple threads trying to download different files.
      */
+    @Test
     public void testFetchRemoteFile_multiConcurrent() throws Exception {
         IFileDownloader mockDownloader1 = Mockito.mock(IFileDownloader.class);
         IFileDownloader mockDownloader2 = Mockito.mock(IFileDownloader.class);
@@ -189,7 +198,7 @@
      * Test {@link FileDownloadCache#fetchRemoteFile(IFileDownloader, String)} being called
      * concurrently by multiple threads trying to download the same file, with one thread failing.
      */
-    @SuppressWarnings("unchecked")
+    @Test
     public void testFetchRemoteFile_concurrentFail() throws Exception {
         // Block first download, and later raise an error, but allow other downloads to pass.
         final AtomicBoolean startedDownload = new AtomicBoolean(false);
@@ -215,6 +224,8 @@
         // will run to completion.
         mMockDownloader.downloadFile(EasyMock.eq(REMOTE_PATH), EasyMock.<File>anyObject());
         EasyMock.expectLastCall().andAnswer(blockedDownloadAnswer).times(2);
+        EasyMock.expect(mMockDownloader.isFresh(EasyMock.anyObject(), EasyMock.eq(REMOTE_PATH)))
+                .andReturn(true);
 
         // Disable thread safety, otherwise the first call will block the rest.
         EasyMock.makeThreadSafe(mMockDownloader, false);
@@ -253,6 +264,7 @@
     }
 
     /** Verify the cache is built from disk contents on creation */
+    @Test
     public void testConstructor_createCache() throws Exception {
         // create cache contents on disk
         File cacheRoot = FileUtil.createTempDir("constructorTest");
@@ -277,9 +289,8 @@
         }
     }
 
-    /**
-     * Test scenario where an already too large cache is built from disk contents.
-     */
+    /** Test scenario where an already too large cache is built from disk contents. */
+    @Test
     public void testConstructor_cacheExceeded() throws Exception {
         File cacheRoot = FileUtil.createTempDir("testConstructor_cacheExceeded");
         try {
diff --git a/tests/src/com/android/tradefed/build/FileDownloadCacheTest.java b/tests/src/com/android/tradefed/build/FileDownloadCacheTest.java
index e2eb215..be65acb 100644
--- a/tests/src/com/android/tradefed/build/FileDownloadCacheTest.java
+++ b/tests/src/com/android/tradefed/build/FileDownloadCacheTest.java
@@ -15,7 +15,11 @@
  */
 package com.android.tradefed.build;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.StreamUtil;
@@ -254,6 +258,54 @@
         EasyMock.verify(mMockDownloader);
     }
 
+    /** Test that when the cache is rebuilt we can find the file without a new download. */
+    @Test
+    public void testCacheRebuild() throws Exception {
+        File cacheDir = FileUtil.createTempDir("cache-unittest");
+        File subDir = FileUtil.createTempDir("subdir", cacheDir);
+        File file = FileUtil.createTempFile("test-cache-file", ".txt", subDir);
+        File cacheFile = null;
+        try {
+            mCache = new FileDownloadCache(cacheDir);
+            setFreshnessExpections(true);
+
+            EasyMock.replay(mMockDownloader);
+            cacheFile =
+                    mCache.fetchRemoteFile(
+                            mMockDownloader, subDir.getName() + "/" + file.getName());
+            assertNotNull(cacheFile);
+            EasyMock.verify(mMockDownloader);
+        } finally {
+            FileUtil.recursiveDelete(cacheDir);
+            FileUtil.deleteFile(cacheFile);
+        }
+    }
+
+    /** Test that keys with multiple slashes are properly handled. */
+    @Test
+    public void testCacheRebuild_multiSlashPath() throws Exception {
+        String gsPath = "foo//bar";
+        // Perform successful download
+        setDownloadExpections(gsPath);
+        EasyMock.replay(mMockDownloader);
+        assertFetchRemoteFile(gsPath, null);
+        EasyMock.verify(mMockDownloader);
+
+        File cachedFile = mCache.getCachedFile(gsPath);
+        try {
+            assertNotNull(cachedFile);
+
+            // Now rebuild the cache and try to find our file
+            mCache = new FileDownloadCache(mCacheDir);
+            File cachedFileRebuilt = mCache.getCachedFile(gsPath);
+            assertNotNull(cachedFileRebuilt);
+
+            assertEquals(cachedFile, cachedFileRebuilt);
+        } finally {
+            FileUtil.deleteFile(cachedFile);
+        }
+    }
+
     /** Perform one fetchRemoteFile call and verify contents for default remote path */
     private void assertFetchRemoteFile() throws BuildRetrievalError, IOException {
         assertFetchRemoteFile(REMOTE_PATH, null);
diff --git a/tests/src/com/android/tradefed/command/CommandSchedulerFuncTest.java b/tests/src/com/android/tradefed/command/CommandSchedulerFuncTest.java
index 2743506..f7485ed 100644
--- a/tests/src/com/android/tradefed/command/CommandSchedulerFuncTest.java
+++ b/tests/src/com/android/tradefed/command/CommandSchedulerFuncTest.java
@@ -16,12 +16,16 @@
 
 package com.android.tradefed.command;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import com.android.ddmlib.IDevice;
 import com.android.ddmlib.Log;
 import com.android.tradefed.config.ConfigurationDescriptor;
+import com.android.tradefed.config.ConfigurationException;
 import com.android.tradefed.config.DeviceConfigurationHolder;
+import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.IConfiguration;
 import com.android.tradefed.config.IConfigurationFactory;
 import com.android.tradefed.config.IDeviceConfiguration;
@@ -42,18 +46,16 @@
 import com.android.tradefed.util.RunUtil;
 import com.android.tradefed.util.keystore.IKeyStoreClient;
 
-import com.google.common.util.concurrent.SettableFuture;
-
 import org.easymock.EasyMock;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.Future;
 
 /** Longer running test for {@link CommandScheduler} */
 @RunWith(JUnit4.class)
@@ -74,6 +76,15 @@
     private boolean mInterruptible = false;
     private IDeviceConfiguration mMockConfig;
 
+    @BeforeClass
+    public static void setUpClass() throws ConfigurationException {
+        try {
+            GlobalConfiguration.createGlobalConfiguration(new String[] {"empty"});
+        } catch (IllegalStateException e) {
+            // ignore
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
         mDeviceOptions = new DeviceSelectionOptions();
@@ -300,15 +311,8 @@
     public void testBatteryLowLevel_interruptible() throws Throwable {
         ITestDevice mockDevice = EasyMock.createNiceMock(ITestDevice.class);
         EasyMock.expect(mockDevice.getSerialNumber()).andReturn("serial").anyTimes();
-        IDevice mockIDevice = new StubDevice("serial") {
-            @Override
-            public Future<Integer> getBattery() {
-                SettableFuture<Integer> f = SettableFuture.create();
-                f.set(10);
-                return f;
-            }
-        };
-
+        IDevice mockIDevice = new StubDevice("serial");
+        EasyMock.expect(mockDevice.getBattery()).andReturn(10);
         EasyMock.expect(mockDevice.getIDevice()).andReturn(mockIDevice).anyTimes();
         EasyMock.expect(mockDevice.getDeviceState()).andReturn(
                 TestDeviceState.ONLINE).anyTimes();
diff --git a/tests/src/com/android/tradefed/config/OptionSetterTest.java b/tests/src/com/android/tradefed/config/OptionSetterTest.java
index 5c26bbc..cbcf407 100644
--- a/tests/src/com/android/tradefed/config/OptionSetterTest.java
+++ b/tests/src/com/android/tradefed/config/OptionSetterTest.java
@@ -36,6 +36,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -272,6 +273,9 @@
     private static class RemoteFileOption {
         @Option(name = "remote-file")
         public File remoteFile = null;
+
+        @Option(name = "remote-file-list")
+        public Collection<File> remoteFileList = new ArrayList<>();
     }
 
     /**
@@ -1018,6 +1022,50 @@
         }
     }
 
+    /** Test {@link OptionSetter#validateGcsFilePath()}. */
+    public void testOptionSetter_remoteFileList() throws ConfigurationException {
+        RemoteFileOption object = new RemoteFileOption();
+        OptionSetter setter =
+                new OptionSetter(object) {
+                    @Override
+                    GCSDownloaderHelper createDownloader() {
+                        return new GCSDownloaderHelper() {
+                            @Override
+                            public File fetchTestResource(String gsPath)
+                                    throws BuildRetrievalError {
+                                try {
+                                    File fake =
+                                            FileUtil.createTempFile("gs-option-setter-test", "txt");
+                                    return fake;
+                                } catch (IOException e) {
+                                    throw new RuntimeException(e);
+                                }
+                            }
+                        };
+                    }
+                };
+        setter.setOptionValue("remote-file-list", "gs://fake/path");
+        setter.setOptionValue("remote-file-list", "fake/file");
+        assertEquals(2, object.remoteFileList.size());
+        Set<File> downloadedFile = setter.validateGcsFilePath();
+        try {
+            assertEquals(1, downloadedFile.size());
+            File downloaded = downloadedFile.iterator().next();
+            // The file has been replaced by the downloaded one.
+            assertEquals(2, object.remoteFileList.size());
+
+            Iterator<File> ite = object.remoteFileList.iterator();
+            File notGsFile = ite.next();
+            assertEquals("fake/file", notGsFile.getPath());
+            File gsFile = ite.next();
+            assertEquals(downloaded.getAbsolutePath(), gsFile.getAbsolutePath());
+        } finally {
+            for (File f : downloadedFile) {
+                FileUtil.recursiveDelete(f);
+            }
+        }
+    }
+
     /**
      * Perform {@link OptionSetter#setOptionValue(String, String)} for a given option.
      */
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 91d3ef5..198ce23 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -3639,6 +3639,44 @@
         assertEquals(Integer.valueOf(10), intArgs.get(1));
     }
 
+    public void testRemoveOwnersWithAdditionalLines() throws Exception {
+        mTestDevice =
+                Mockito.spy(
+                        new TestableTestDevice() {
+                            @Override
+                            public String executeShellCommand(String command)
+                                    throws DeviceNotAvailableException {
+                                return "Current Device Policy Manager state:\n"
+                                        + "  Device Owner: \n"
+                                        + "    admin=ComponentInfo{aaa/aaa}\n"
+                                        + "    name=\n"
+                                        + "    package=aaa\n"
+                                        + "    moreLines=true\n"
+                                        + "    User ID: 0\n"
+                                        + "\n"
+                                        + "  Profile Owner (User 10): \n"
+                                        + "    admin=ComponentInfo{bbb/bbb}\n"
+                                        + "    name=bbb\n"
+                                        + "    package=bbb\n";
+                            }
+                        });
+        mTestDevice.removeOwners();
+
+        // Verified removeAdmin is called to remove owners.
+        ArgumentCaptor<String> stringCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<Integer> intCaptor = ArgumentCaptor.forClass(Integer.class);
+        Mockito.verify(mTestDevice, Mockito.times(2))
+                .removeAdmin(stringCaptor.capture(), intCaptor.capture());
+        List<String> stringArgs = stringCaptor.getAllValues();
+        List<Integer> intArgs = intCaptor.getAllValues();
+
+        assertEquals("aaa/aaa", stringArgs.get(0));
+        assertEquals(Integer.valueOf(0), intArgs.get(0));
+
+        assertEquals("bbb/bbb", stringArgs.get(1));
+        assertEquals(Integer.valueOf(10), intArgs.get(1));
+    }
+
     /** Test that the output of cryptfs allows for encryption for newest format. */
     public void testIsEncryptionSupported_newformat() throws Exception {
         mTestDevice =
diff --git a/tests/src/com/android/tradefed/device/helper/TelephonyHelperTest.java b/tests/src/com/android/tradefed/device/helper/TelephonyHelperTest.java
new file mode 100644
index 0000000..c36d7b5
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/helper/TelephonyHelperTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.device.helper;
+
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.ddmlib.IDevice;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.helper.TelephonyHelper.SimCardInformation;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ITestInvocationListener;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashMap;
+
+/** Unit tests for {@link TelephonyHelper}. */
+@RunWith(JUnit4.class)
+public class TelephonyHelperTest {
+
+    private ITestDevice mDevice;
+    private IDevice mMockIDevice;
+
+    @Before
+    public void setUp() {
+        mDevice = EasyMock.createMock(ITestDevice.class);
+        mMockIDevice = EasyMock.createMock(IDevice.class);
+
+        EasyMock.expect(mDevice.getIDevice()).andStubReturn(mMockIDevice);
+    }
+
+    @Test
+    public void testGetSimInfo() throws Exception {
+        EasyMock.expect(mDevice.installPackage(EasyMock.anyObject(), EasyMock.eq(true)))
+                .andReturn(null);
+        EasyMock.expect(mDevice.uninstallPackage(TelephonyHelper.PACKAGE_NAME)).andReturn(null);
+
+        EasyMock.expect(
+                        mDevice.runInstrumentationTests(
+                                EasyMock.anyObject(),
+                                (ITestInvocationListener) EasyMock.anyObject()))
+                .andAnswer(
+                        new IAnswer<Boolean>() {
+
+                            @Override
+                            public Boolean answer() throws Throwable {
+                                ITestInvocationListener collector =
+                                        (ITestInvocationListener) getCurrentArguments()[1];
+                                collector.testRunStarted("android.telephony.utility", 1);
+                                collector.testStarted(TelephonyHelper.SIM_TEST);
+                                HashMap<String, String> testMetrics = new HashMap<>();
+                                testMetrics.put(TelephonyHelper.SIM_STATE_KEY, "5");
+                                testMetrics.put(TelephonyHelper.CARRIER_PRIVILEGES_KEY, "true");
+                                collector.testEnded(TelephonyHelper.SIM_TEST, testMetrics);
+                                collector.testRunEnded(500L, new HashMap<String, Metric>());
+                                return true;
+                            }
+                        });
+        EasyMock.replay(mDevice, mMockIDevice);
+        SimCardInformation info = TelephonyHelper.getSimInfo(mDevice);
+        assertTrue(info.mCarrierPrivileges);
+        assertTrue(info.mHasTelephonySupport);
+        assertEquals("5", info.mSimState);
+        EasyMock.verify(mDevice, mMockIDevice);
+    }
+
+    @Test
+    public void testGetSimInfo_installFail() throws Exception {
+        EasyMock.expect(mDevice.installPackage(EasyMock.anyObject(), EasyMock.eq(true)))
+                .andReturn("Failed to install");
+
+        EasyMock.replay(mDevice, mMockIDevice);
+        assertNull(TelephonyHelper.getSimInfo(mDevice));
+        EasyMock.verify(mDevice, mMockIDevice);
+    }
+
+    @Test
+    public void testGetSimInfo_instrumentationFailed() throws Exception {
+        EasyMock.expect(mDevice.installPackage(EasyMock.anyObject(), EasyMock.eq(true)))
+                .andReturn(null);
+        EasyMock.expect(mDevice.uninstallPackage(TelephonyHelper.PACKAGE_NAME)).andReturn(null);
+
+        EasyMock.expect(
+                        mDevice.runInstrumentationTests(
+                                EasyMock.anyObject(),
+                                (ITestInvocationListener) EasyMock.anyObject()))
+                .andAnswer(
+                        new IAnswer<Boolean>() {
+
+                            @Override
+                            public Boolean answer() throws Throwable {
+                                ITestInvocationListener collector =
+                                        (ITestInvocationListener) getCurrentArguments()[1];
+                                collector.testRunStarted("android.telephony.utility", 1);
+                                collector.testRunFailed("Couldn't run the instrumentation.");
+                                collector.testRunEnded(500L, new HashMap<String, Metric>());
+                                return true;
+                            }
+                        });
+        EasyMock.replay(mDevice, mMockIDevice);
+        assertNull(TelephonyHelper.getSimInfo(mDevice));
+        EasyMock.verify(mDevice, mMockIDevice);
+    }
+
+    @Test
+    public void testGetSimInfo_simTest_not_run() throws Exception {
+        EasyMock.expect(mDevice.installPackage(EasyMock.anyObject(), EasyMock.eq(true)))
+                .andReturn(null);
+        EasyMock.expect(mDevice.uninstallPackage(TelephonyHelper.PACKAGE_NAME)).andReturn(null);
+
+        EasyMock.expect(
+                        mDevice.runInstrumentationTests(
+                                EasyMock.anyObject(),
+                                (ITestInvocationListener) EasyMock.anyObject()))
+                .andAnswer(
+                        new IAnswer<Boolean>() {
+
+                            @Override
+                            public Boolean answer() throws Throwable {
+                                ITestInvocationListener collector =
+                                        (ITestInvocationListener) getCurrentArguments()[1];
+                                collector.testRunStarted("android.telephony.utility", 1);
+                                collector.testRunEnded(500L, new HashMap<String, Metric>());
+                                return true;
+                            }
+                        });
+        EasyMock.replay(mDevice, mMockIDevice);
+        assertNull(TelephonyHelper.getSimInfo(mDevice));
+        EasyMock.verify(mDevice, mMockIDevice);
+    }
+
+    @Test
+    public void testGetSimInfo_simTest_failed() throws Exception {
+        EasyMock.expect(mDevice.installPackage(EasyMock.anyObject(), EasyMock.eq(true)))
+                .andReturn(null);
+        EasyMock.expect(mDevice.uninstallPackage(TelephonyHelper.PACKAGE_NAME)).andReturn(null);
+
+        EasyMock.expect(
+                        mDevice.runInstrumentationTests(
+                                EasyMock.anyObject(),
+                                (ITestInvocationListener) EasyMock.anyObject()))
+                .andAnswer(
+                        new IAnswer<Boolean>() {
+
+                            @Override
+                            public Boolean answer() throws Throwable {
+                                ITestInvocationListener collector =
+                                        (ITestInvocationListener) getCurrentArguments()[1];
+                                collector.testRunStarted("android.telephony.utility", 1);
+                                collector.testStarted(TelephonyHelper.SIM_TEST);
+                                collector.testFailed(
+                                        TelephonyHelper.SIM_TEST, "No TelephonyManager");
+                                collector.testEnded(
+                                        TelephonyHelper.SIM_TEST, new HashMap<String, String>());
+                                collector.testRunEnded(500L, new HashMap<String, Metric>());
+                                return true;
+                            }
+                        });
+        EasyMock.replay(mDevice, mMockIDevice);
+        SimCardInformation info = TelephonyHelper.getSimInfo(mDevice);
+        assertFalse(info.mHasTelephonySupport);
+        EasyMock.verify(mDevice, mMockIDevice);
+    }
+}
diff --git a/tests/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollectorTest.java
index 61f963e..60b955f 100644
--- a/tests/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollectorTest.java
+++ b/tests/src/com/android/tradefed/device/metric/FilePullerDeviceMetricCollectorTest.java
@@ -1,20 +1,8 @@
-/*
- * 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 com.android.tradefed.device.metric;
 
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.invoker.IInvocationContext;
@@ -23,6 +11,7 @@
 import com.android.tradefed.result.FileInputStreamSource;
 import com.android.tradefed.result.ITestInvocationListener;
 import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.util.FileUtil;
 import com.android.tradefed.util.proto.TfMetricProtoUtil;
 
@@ -54,14 +43,14 @@
                 new FilePullerDeviceMetricCollector() {
                     @Override
                     public void processMetricFile(
-                            String key, File metricFile, DeviceMetricData runData) {
+                            String key, File metricFile, DeviceMetricData data) {
                         try (FileInputStreamSource source = new FileInputStreamSource(metricFile)) {
                             testLog(key, LogDataType.TEXT, source);
                         }
                     }
                     @Override
                     public void processMetricDirectory(
-                            String key, File metricDirectory, DeviceMetricData runData) {
+                            String key, File metricDirectory, DeviceMetricData data) {
                         try (FileInputStreamSource source = new FileInputStreamSource(
                                 metricDirectory)) {
                             testLog(key, LogDataType.TEXT, source);
@@ -123,6 +112,35 @@
                 .testLog(Mockito.eq("coverageFile"), Mockito.eq(LogDataType.TEXT), Mockito.any());
     }
 
+    /**
+     * Test {@link FilePullerDeviceMetricCollector#processMetricFile(String, File,
+     * Map<String, Metric>)} is called on test case end and test run ended.
+     */
+    @Test
+    public void testMetricFileProcessingFlow() throws Exception {
+        OptionSetter setter = new OptionSetter(mFilePuller);
+        setter.setOptionValue("pull-pattern-keys", "coverageFile");
+        HashMap<String, Metric> currentMetrics = new HashMap<>();
+        currentMetrics.put("coverageFile", TfMetricProtoUtil.stringToMetric("/data/coverage"));
+
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/coverage")))
+                .thenReturn(new File("fake"));
+
+        TestDescription testDesc = new TestDescription("xyz", "abc");
+
+        mFilePuller.testRunStarted("fakeRun", 5);
+        mFilePuller.testStarted(testDesc);
+        mFilePuller.testEnded(testDesc, currentMetrics);
+        Mockito.verify(mMockListener)
+                .testLog(Mockito.eq("coverageFile"), Mockito.eq(LogDataType.TEXT), Mockito.any());
+        verify(mMockListener, times(1)).testLog(Mockito.eq("coverageFile"),
+                Mockito.eq(LogDataType.TEXT), Mockito.any());
+        mFilePuller.testRunEnded(500, currentMetrics);
+        verify(mMockListener, times(2)).testLog(Mockito.eq("coverageFile"),
+                Mockito.eq(LogDataType.TEXT), Mockito.any());
+
+    }
+
     /** Test when a file exists in the metrics but the pattern searching does not match it. */
     @Test
     public void testPatternNotMatching() throws Exception {
diff --git a/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
new file mode 100644
index 0000000..3945260
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/metric/PerfettoPullerMetricCollectorTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tradefed.device.metric;
+
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.TestDescription;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.proto.TfMetricProtoUtil;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.util.HashMap;
+
+/** Unit tests for {@link PerfettoPullerMetricCollector}. */
+@RunWith(JUnit4.class)
+public class PerfettoPullerMetricCollectorTest {
+
+    private PerfettoPullerMetricCollector mPerfettoMetricCollector;
+    @Mock
+    private ITestInvocationListener mMockListener;
+    @Mock
+    private ITestDevice mMockDevice;
+    private IInvocationContext mContext;
+
+
+    @Before
+    public void setUp() {
+
+        MockitoAnnotations.initMocks(this);
+        mContext = new InvocationContext();
+        mContext.addAllocatedDevice("default", mMockDevice);
+        mPerfettoMetricCollector = Mockito.spy(new PerfettoPullerMetricCollector());
+        mPerfettoMetricCollector.init(mContext, mMockListener);
+    }
+
+    @Test
+    public void testNoProcessingFlow() throws Exception {
+
+        OptionSetter setter = new OptionSetter(mPerfettoMetricCollector);
+        setter.setOptionValue("pull-pattern-keys", "perfettofile");
+        HashMap<String, Metric> currentMetrics = new HashMap<>();
+        currentMetrics.put("perfettofile", TfMetricProtoUtil.stringToMetric("/data/trace.pb"));
+
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/trace.pb")))
+                .thenReturn(new File("trace"));
+
+        TestDescription testDesc = new TestDescription("xyz", "abc");
+        mPerfettoMetricCollector.testStarted(testDesc);
+        mPerfettoMetricCollector.testEnded(testDesc, currentMetrics);
+
+        Mockito.verify(mMockListener)
+                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+    }
+
+    @Test
+    public void testProcessingFlow() throws Exception {
+
+        OptionSetter setter = new OptionSetter(mPerfettoMetricCollector);
+        setter.setOptionValue("pull-pattern-keys", "perfettofile");
+        setter.setOptionValue("perfetto-binary-path", "trx");
+        HashMap<String, Metric> currentMetrics = new HashMap<>();
+        currentMetrics.put("perfettofile", TfMetricProtoUtil.stringToMetric("/data/trace.pb"));
+        Mockito.when(mMockDevice.pullFile(Mockito.eq("/data/trace.pb")))
+                .thenReturn(new File("trace"));
+
+        TestDescription testDesc = new TestDescription("xyz", "abc");
+        CommandResult cr = new CommandResult();
+        cr.setStatus(CommandStatus.SUCCESS);
+        cr.setStdout("abc:efg");
+
+        Mockito.doReturn(cr).when(mPerfettoMetricCollector).runHostCommand(Mockito.any());
+
+        mPerfettoMetricCollector.testStarted(testDesc);
+        mPerfettoMetricCollector.testEnded(testDesc, currentMetrics);
+
+        Mockito.verify(mPerfettoMetricCollector).runHostCommand(Mockito.any());
+        Mockito.verify(mMockListener)
+                .testLog(Mockito.eq("trace"), Mockito.eq(LogDataType.PB), Mockito.any());
+    }
+}
diff --git a/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java b/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
index 6c33f6f..3aa47c3 100644
--- a/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
+++ b/tests/src/com/android/tradefed/invoker/InvocationExecutionTest.java
@@ -16,10 +16,17 @@
 package com.android.tradefed.invoker;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 
+import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.build.IBuildProvider;
+import com.android.tradefed.build.StubBuildProvider;
 import com.android.tradefed.config.Configuration;
 import com.android.tradefed.config.DeviceConfigurationHolder;
 import com.android.tradefed.config.IConfiguration;
@@ -45,6 +52,7 @@
 import org.mockito.InOrder;
 import org.mockito.Mockito;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -60,6 +68,7 @@
     private IInvocationContext mContext;
     private IConfiguration mConfig;
     private ITestInvocationListener mMockListener;
+    private ITestDevice mMockDevice;
 
     @Before
     public void setUp() {
@@ -67,6 +76,7 @@
         mContext = new InvocationContext();
         mConfig = new Configuration("test", "test");
         mMockListener = mock(ITestInvocationListener.class);
+        mMockDevice = EasyMock.createMock(ITestDevice.class);
     }
 
     /** Test class for a target preparer class that also do host cleaner. */
@@ -318,4 +328,46 @@
         inOrder.verify(stub1).isDisabled();
         inOrder.verify(stub1).tearDown(mContext, exception);
     }
+
+    /** Ensure we create the shared folder from the resource build. */
+    @Test
+    public void testFetchBuild_createSharedFolder() throws Throwable {
+        EasyMock.expect(mMockDevice.getSerialNumber()).andStubReturn("serial");
+        mMockDevice.setRecovery(EasyMock.anyObject());
+        EasyMock.expectLastCall().times(2);
+
+        List<IDeviceConfiguration> listDeviceConfig = new ArrayList<>();
+        DeviceConfigurationHolder holder = new DeviceConfigurationHolder("device1");
+        IBuildProvider provider = new StubBuildProvider();
+        holder.addSpecificConfig(provider);
+        mContext.addAllocatedDevice("device1", mMockDevice);
+        listDeviceConfig.add(holder);
+
+        DeviceConfigurationHolder holder2 = new DeviceConfigurationHolder("device2", true);
+        IBuildProvider provider2 = new StubBuildProvider();
+        holder2.addSpecificConfig(provider2);
+        mContext.addAllocatedDevice("device2", mMockDevice);
+        listDeviceConfig.add(holder2);
+
+        mConfig.setDeviceConfigList(listDeviceConfig);
+        // Download
+        EasyMock.replay(mMockDevice);
+        assertTrue(mExec.fetchBuild(mContext, mConfig, null, mMockListener));
+        EasyMock.verify(mMockDevice);
+
+        List<IBuildInfo> builds = mContext.getBuildInfos();
+        try {
+            assertEquals(2, builds.size());
+            IBuildInfo realBuild = builds.get(0);
+            File shared = realBuild.getFile(BuildInfoFileKey.SHARED_RESOURCE_DIR);
+            assertNotNull(shared);
+
+            IBuildInfo fakeBuild = builds.get(1);
+            assertNull(fakeBuild.getFile(BuildInfoFileKey.SHARED_RESOURCE_DIR));
+        } finally {
+            for (IBuildInfo info : builds) {
+                info.cleanUp();
+            }
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
index 4361d24..5482801 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationMultiTest.java
@@ -116,6 +116,7 @@
         mProvider1 = EasyMock.createMock(IBuildProvider.class);
         holder1.addSpecificConfig(mProvider1);
         EasyMock.expect(mMockConfig.getDeviceConfigByName("device1")).andStubReturn(holder1);
+        EasyMock.expect(mMockConfig.isDeviceConfiguredFake("device1")).andReturn(false);
         mDevice1.setOptions(EasyMock.anyObject());
         mDevice1.setRecovery(EasyMock.anyObject());
 
diff --git a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
index fb54c8e..9ba402c 100644
--- a/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
+++ b/tests/src/com/android/tradefed/invoker/TestInvocationTest.java
@@ -209,6 +209,8 @@
         EasyMock.expect(mMockBuildInfo.getBuildFlavor()).andStubReturn("flavor");
         EasyMock.expect(mMockBuildInfo.getProperties()).andStubReturn(new HashSet<>());
         EasyMock.expect(mMockBuildInfo.isTestResourceBuild()).andStubReturn(false);
+        mMockBuildInfo.setTestResourceBuild(EasyMock.anyBoolean());
+        EasyMock.expectLastCall().anyTimes();
 
         // always expect logger initialization and cleanup calls
         mMockLogRegistry.registerLogger(mMockLogger);
@@ -1526,6 +1528,9 @@
         mMockBuildInfo = EasyMock.createMock(IDeviceBuildInfo.class);
         EasyMock.expect(mMockBuildInfo.getProperties()).andStubReturn(new HashSet<>());
         EasyMock.expect(mMockBuildInfo.isTestResourceBuild()).andStubReturn(false);
+        mMockBuildInfo.setTestResourceBuild(EasyMock.anyBoolean());
+        EasyMock.expectLastCall().anyTimes();
+
         IRemoteTest test = EasyMock.createNiceMock(IRemoteTest.class);
         ITargetCleaner mockCleaner = EasyMock.createMock(ITargetCleaner.class);
         EasyMock.expect(mockCleaner.isDisabled()).andReturn(false).times(2);
@@ -1611,6 +1616,8 @@
                     .andReturn(tmpTestsDir);
             EasyMock.expect(mMockBuildInfo.getProperties()).andStubReturn(new HashSet<>());
             EasyMock.expect(mMockBuildInfo.isTestResourceBuild()).andStubReturn(false);
+            mMockBuildInfo.setTestResourceBuild(EasyMock.anyBoolean());
+            EasyMock.expectLastCall().anyTimes();
 
             setupMockSuccessListeners();
             setupNormalInvoke(test);
@@ -1691,6 +1698,8 @@
             prop.add(BuildInfoProperties.DO_NOT_LINK_TESTS_DIR);
             EasyMock.expect(mMockBuildInfo.getProperties()).andStubReturn(prop);
             EasyMock.expect(mMockBuildInfo.isTestResourceBuild()).andStubReturn(false);
+            mMockBuildInfo.setTestResourceBuild(EasyMock.anyBoolean());
+            EasyMock.expectLastCall().anyTimes();
 
             setupMockSuccessListeners();
             setupNormalInvoke(test);
diff --git a/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java b/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
index c24b2d0..384b04a 100644
--- a/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
+++ b/tests/src/com/android/tradefed/postprocessor/AggregatePostProcessorTest.java
@@ -18,12 +18,13 @@
 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
 import com.android.tradefed.result.TestDescription;
 
+import com.google.common.collect.ImmutableList;
+
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import com.google.common.collect.ImmutableList;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -234,6 +235,7 @@
                         .getSingleString());
     }
 
+
     /** Test that non-numeric metric does not show up in the reported results. */
     @Test
     public void testNonNumericMetric() {
@@ -392,6 +394,83 @@
                         .getSingleString());
     }
 
+
+    /**
+     *  Test successful processed run metrics when there are more than one comma
+     *  separated value.
+     */
+    @Test
+    public void testSuccessfullProcessRunMetrics() {
+        final String key = "single_run";
+        final String value = "1.00, 2.00";
+
+        HashMap<String, Metric> runMetrics = new HashMap<String, Metric>();
+        Metric.Builder metricBuilder = Metric.newBuilder();
+        metricBuilder.getMeasurementsBuilder().setSingleString(value);
+        Metric currentRunMetric = metricBuilder.build();
+        runMetrics.put(key, currentRunMetric);
+        Map<String, Metric.Builder> processedMetrics =
+                mProcessor.processRunMetrics(runMetrics);
+
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_MIN)));
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_MAX)));
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_MEAN)));
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_VAR)));
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_STDEV)));
+        Assert.assertTrue(
+                processedMetrics.containsKey(
+                        String.join(STATS_KEY_SEPARATOR, key, STATS_KEY_MEDIAN)));
+    }
+
+    /**
+     *  Test empty processed run metrics when there is one double value associated with
+     *  the key.
+     */
+    @Test
+    public void testSingleValueProcessRunMetrics() {
+        final String key = "single_run";
+        final String value = "1.00";
+
+        HashMap<String, Metric> runMetrics = new HashMap<String, Metric>();
+        Metric.Builder metricBuilder = Metric.newBuilder();
+        metricBuilder.getMeasurementsBuilder().setSingleString(value);
+        Metric currentRunMetric = metricBuilder.build();
+        runMetrics.put(key, currentRunMetric);
+        Map<String, Metric.Builder> processedMetrics =
+                mProcessor.processRunMetrics(runMetrics);
+
+        Assert.assertEquals(0, processedMetrics.size());
+    }
+
+    /**
+     *  Test non double run metrics values return empty processed metrics.
+     */
+    @Test
+    public void testNoDoubleProcessRunMetrics() {
+        final String key = "single_run";
+        final String value = "1.00, abc";
+
+        HashMap<String, Metric> runMetrics = new HashMap<String, Metric>();
+        Metric.Builder metricBuilder = Metric.newBuilder();
+        metricBuilder.getMeasurementsBuilder().setSingleString(value);
+        Metric currentRunMetric = metricBuilder.build();
+        runMetrics.put(key, currentRunMetric);
+        Map<String, Metric.Builder> processedMetrics =
+                mProcessor.processRunMetrics(runMetrics);
+
+        Assert.assertEquals(0, processedMetrics.size());
+    }
+
     /** Test that metrics are correctly aggregated for different tests. */
     @Test
     public void testDifferentTests() {
diff --git a/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java b/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java
new file mode 100644
index 0000000..591a17b
--- /dev/null
+++ b/tests/src/com/android/tradefed/result/proto/FileProtoResultReporterTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.result.proto;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.config.ConfigurationDescriptor;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.invoker.InvocationContext;
+import com.android.tradefed.invoker.proto.InvocationContext.Context;
+import com.android.tradefed.result.proto.TestRecordProto.TestRecord;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.proto.TestRecordProtoUtil;
+
+import com.google.protobuf.Any;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+/** Unit tests for {@link FileProtoResultReporter}. */
+@RunWith(JUnit4.class)
+public class FileProtoResultReporterTest {
+
+    private FileProtoResultReporter mReporter;
+    private File mOutput;
+
+    @Before
+    public void setUp() throws Exception {
+        mOutput = FileUtil.createTempFile("proto-file-reporter-test", ".pb");
+        mReporter = new FileProtoResultReporter();
+        mReporter.setFileOutput(mOutput);
+    }
+
+    @After
+    public void tearDown() {
+        FileUtil.deleteFile(mOutput);
+    }
+
+    @Test
+    public void testWriteResults() throws Exception {
+        assertEquals(0L, mOutput.length());
+        IInvocationContext context = new InvocationContext();
+        context.setConfigurationDescriptor(new ConfigurationDescriptor());
+        context.addInvocationAttribute("test", "test");
+        mReporter.invocationStarted(context);
+        mReporter.invocationEnded(500L);
+
+        // Something was outputted
+        assertTrue(mOutput.length() != 0L);
+        TestRecord record = TestRecordProtoUtil.readFromFile(mOutput);
+
+        Any anyDescription = record.getDescription();
+        assertTrue(anyDescription.is(Context.class));
+
+        IInvocationContext endContext =
+                InvocationContext.fromProto(anyDescription.unpack(Context.class));
+        assertEquals("test", endContext.getAttributes().get("test").get(0));
+    }
+}
diff --git a/tests/src/com/android/tradefed/suite/checker/ShellStatusCheckerTest.java b/tests/src/com/android/tradefed/suite/checker/ShellStatusCheckerTest.java
new file mode 100644
index 0000000..0b49ebd
--- /dev/null
+++ b/tests/src/com/android/tradefed/suite/checker/ShellStatusCheckerTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.tradefed.suite.checker;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.suite.checker.StatusCheckerResult.CheckStatus;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link ShellStatusChecker}.
+ *
+ * <pre> Run with:
+ * cd ${ANDROID_BUILD_TOP}/tools/tradefederation/core/tests
+ * mm
+ * run_tradefed_tests.sh --class com.android.tradefed.suite.checker.ShellStatusCheckerTest
+ * </pre>
+ */
+@RunWith(JUnit4.class)
+public class ShellStatusCheckerTest {
+
+    private ShellStatusChecker mChecker;
+    private ITestDevice mMockDevice;
+    private static final String FAIL_STRING = "Reset failed";
+
+    /* The expected root state at pre- and post-check. */
+    private boolean mExpectRoot = false;
+
+    @Before
+    public void setUp() {
+        mMockDevice = EasyMock.createMock(ITestDevice.class);
+        mChecker = new ShellStatusChecker();
+    }
+
+    /* Verify pre- and post-state easily. Shorthand to always revert ok. */
+    private void expectPreAndPost(boolean preRoot, boolean postRoot) throws Exception {
+        expectPreAndPost(preRoot, postRoot, true, true);
+    }
+
+    /* Verify revert-state easily. Shorthand to fail expectation and verify revert result. */
+    private void expectRevertOk(boolean preRevertOk, boolean postRevertOk) throws Exception {
+        expectPreAndPost(!mExpectRoot, !mExpectRoot, preRevertOk, postRevertOk);
+    }
+
+    /* Extended helper to verify pre- and post-state fully. */
+    private void expectPreAndPost(
+            boolean preRoot, boolean postRoot, boolean preRevertOk, boolean postRevertOk)
+            throws Exception {
+        CheckStatus preState = CheckStatus.SUCCESS;
+        CheckStatus postState = CheckStatus.SUCCESS;
+
+        EasyMock.expect(mMockDevice.isAdbRoot()).andReturn(preRoot);
+        if (preRoot != mExpectRoot) {
+            preState = CheckStatus.FAILED;
+            if (mExpectRoot) {
+                EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(preRevertOk);
+            } else {
+                EasyMock.expect(mMockDevice.disableAdbRoot()).andReturn(preRevertOk);
+            }
+        }
+
+        EasyMock.expect(mMockDevice.isAdbRoot()).andReturn(postRoot);
+        if (postRoot != mExpectRoot) {
+            postState = CheckStatus.FAILED;
+            if (mExpectRoot) {
+                EasyMock.expect(mMockDevice.enableAdbRoot()).andReturn(postRevertOk);
+            } else {
+                EasyMock.expect(mMockDevice.disableAdbRoot()).andReturn(postRevertOk);
+            }
+        }
+
+        EasyMock.replay(mMockDevice);
+
+        StatusCheckerResult res = mChecker.preExecutionCheck(mMockDevice);
+        assertEquals("preExecutionCheck1", preState, res.getStatus());
+        String msg = res.getErrorMessage();
+        // Assume that preRevertOk flag is only false when the intention was to test it.
+        if (preState != CheckStatus.SUCCESS || !preRevertOk) {
+            assertNotNull("preExecutionCheck2", msg);
+            assertEquals("preExecutionCheck3", !preRevertOk, msg.contains(FAIL_STRING));
+        } else {
+            assertNull("preExecutionCheck4", msg);
+        }
+
+        res = mChecker.postExecutionCheck(mMockDevice);
+        assertEquals("postExecutionCheck1", postState, res.getStatus());
+        msg = res.getErrorMessage();
+        if (postState != CheckStatus.SUCCESS || !postRevertOk) {
+            assertNotNull("postExecutionCheck2", msg);
+            assertEquals("postExecutionCheck3", !postRevertOk, msg.contains(FAIL_STRING));
+        } else {
+            assertNull("postExecutionCheck4", msg);
+        }
+
+        EasyMock.verify(mMockDevice);
+    }
+
+    /** Test the system checker if the non-expected state does not change. */
+    @Test
+    public void testUnexpectedRemainUnchanged() throws Exception {
+        setExpectedRoot(false);
+        expectPreAndPost(false, false);
+    }
+
+    /** Test the system checker if the non-expected state changes. */
+    @Test
+    public void testUnexpectedChanged() throws Exception {
+        setExpectedRoot(true);
+        expectPreAndPost(false, true);
+    }
+
+    /** Test the system checker if the expected state does not change. */
+    @Test
+    public void testExpectedRemainUnchanged() throws Exception {
+        setExpectedRoot(true);
+        expectPreAndPost(true, true);
+    }
+
+    /** Test the system checker if the expected state changes. */
+    @Test
+    public void testExpectedChanged() throws Exception {
+        setExpectedRoot(false);
+        expectPreAndPost(true, false);
+    }
+
+    /** Test that error message warns for every revert failed. */
+    @Test
+    public void testRevertFails() throws Exception {
+        setExpectedRoot(false);
+        expectRevertOk(false, false);
+    }
+
+    /** Test that error message warns for failed reverts independently. */
+    @Test
+    public void testRevertFailIndependant() throws Exception {
+        setExpectedRoot(true);
+        expectRevertOk(true, false);
+    }
+
+    private void setExpectedRoot(boolean expectRoot) throws ConfigurationException {
+        mExpectRoot = expectRoot;
+        OptionSetter setter = new OptionSetter(mChecker);
+        setter.setOptionValue("expect-root", Boolean.toString(expectRoot));
+    }
+}
diff --git a/tests/src/com/android/tradefed/testtype/GTestXmlResultParserTest.java b/tests/src/com/android/tradefed/testtype/GTestXmlResultParserTest.java
index b440201..8e0efbd 100644
--- a/tests/src/com/android/tradefed/testtype/GTestXmlResultParserTest.java
+++ b/tests/src/com/android/tradefed/testtype/GTestXmlResultParserTest.java
@@ -22,9 +22,10 @@
 import com.android.tradefed.result.TestDescription;
 import com.android.tradefed.util.FileUtil;
 
-import junit.framework.TestCase;
-
 import org.easymock.EasyMock;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -33,7 +34,8 @@
 import java.util.HashMap;
 
 /** Unit tests for {@link GTestXmlResultParser} */
-public class GTestXmlResultParserTest extends TestCase {
+@RunWith(JUnit4.class)
+public class GTestXmlResultParserTest {
     private static final String TEST_TYPE_DIR = "testtype";
     private static final String TEST_MODULE_NAME = "module";
     private static final String GTEST_OUTPUT_FILE_1 = "gtest_output1.xml";
@@ -41,6 +43,7 @@
     private static final String GTEST_OUTPUT_FILE_3 = "gtest_output3.xml";
     private static final String GTEST_OUTPUT_FILE_4 = "gtest_output4.xml";
     private static final String GTEST_OUTPUT_FILE_5 = "gtest_output5.xml";
+    private static final String GTEST_OUTPUT_FILE_6 = "gtest_output6.xml";
 
     /**
      * Helper to read a file from the res/testtype directory and return the associated {@link File}
@@ -70,10 +73,9 @@
         return res;
     }
 
-    /**
-     * Tests the parser for a simple test run output with 6 tests.
-     */
+    /** Tests the parser for a simple test run output with 6 tests. */
     @SuppressWarnings("unchecked")
+    @Test
     public void testParseSimpleFile() throws Exception {
         File contents =  readInFile(GTEST_OUTPUT_FILE_1);
         TestDescription firstInFile = new TestDescription("InteropTest", "test_lookup_hit");
@@ -103,10 +105,9 @@
         }
     }
 
-    /**
-     * Tests the parser for a run with 84 tests.
-     */
+    /** Tests the parser for a run with 84 tests. */
     @SuppressWarnings("unchecked")
+    @Test
     public void testParseLargerFile() throws Exception {
         File contents =  readInFile(GTEST_OUTPUT_FILE_2);
         try {
@@ -132,10 +133,9 @@
         }
     }
 
-    /**
-     * Tests the parser for a run with test failures.
-     */
+    /** Tests the parser for a run with test failures. */
     @SuppressWarnings("unchecked")
+    @Test
     public void testParseWithFailures() throws Exception {
         String expectedMessage = "Message\nFailed";
 
@@ -170,10 +170,9 @@
         }
     }
 
-    /**
-     * Tests the parser for a run with a bad file, as if the test hadn't outputed.
-     */
+    /** Tests the parser for a run with a bad file, as if the test hadn't outputed. */
     @SuppressWarnings("unchecked")
+    @Test
     public void testParseWithEmptyFile() throws Exception {
         String expected = "Failed to get an xml output from tests, it probably crashed";
 
@@ -195,10 +194,9 @@
         }
     }
 
-    /**
-     * Tests the parser for a simple test run output with 6 tests but report expected 7.
-     */
+    /** Tests the parser for a simple test run output with 6 tests but report expected 7. */
     @SuppressWarnings("unchecked")
+    @Test
     public void testParseUnexpectedNumberTest() throws Exception {
         String expected = "Test run incomplete. Expected 7 tests, received 6";
         File contents =  readInFile(GTEST_OUTPUT_FILE_4);
@@ -231,6 +229,7 @@
      * won't be parsed.
      */
     @SuppressWarnings("unchecked")
+    @Test
     public void testParseSimpleFile_badXmltag() throws Exception {
         String expected = "Test run incomplete. Expected 6 tests, received 3";
         File contents =  readInFile(GTEST_OUTPUT_FILE_5);
@@ -258,10 +257,9 @@
         }
     }
 
-    /**
-     * Tests the parser for a run with a bad file, with Collector output to get some logs.
-     */
+    /** Tests the parser for a run with a bad file, with Collector output to get some logs. */
     @SuppressWarnings("unchecked")
+    @Test
     public void testParseWithEmptyFile_AdditionalOutput() throws Exception {
         final String exec_log = "EXECUTION LOG";
         CollectingOutputReceiver fake = new CollectingOutputReceiver() {
@@ -290,4 +288,39 @@
             FileUtil.deleteFile(contents);
         }
     }
+
+    /** Ensure that skipped status is properly carried. */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testParseSimpleFile_skipped() throws Exception {
+        File contents = readInFile(GTEST_OUTPUT_FILE_6);
+        TestDescription firstInFile = new TestDescription("InteropTest", "test_lookup_hit");
+        try {
+            ITestInvocationListener mockRunListener =
+                    EasyMock.createMock(ITestInvocationListener.class);
+            mockRunListener.testRunStarted(TEST_MODULE_NAME, 6);
+            mockRunListener.testStarted(firstInFile);
+            mockRunListener.testEnded(
+                    EasyMock.eq(firstInFile), (HashMap<String, Metric>) EasyMock.anyObject());
+            // 5 more passing test cases in this run
+            for (int i = 0; i < 5; ++i) {
+                mockRunListener.testStarted((TestDescription) EasyMock.anyObject());
+                if (i == 1) {
+                    mockRunListener.testIgnored((TestDescription) EasyMock.anyObject());
+                }
+                mockRunListener.testEnded(
+                        (TestDescription) EasyMock.anyObject(),
+                        (HashMap<String, Metric>) EasyMock.anyObject());
+            }
+            mockRunListener.testRunEnded(
+                    EasyMock.anyLong(), (HashMap<String, Metric>) EasyMock.anyObject());
+            EasyMock.replay(mockRunListener);
+            GTestXmlResultParser resultParser =
+                    new GTestXmlResultParser(TEST_MODULE_NAME, mockRunListener);
+            resultParser.parseResult(contents, null);
+            EasyMock.verify(mockRunListener);
+        } finally {
+            FileUtil.deleteFile(contents);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/testtype/metricregression/DetectRegressionTest.java b/tests/src/com/android/tradefed/testtype/metricregression/DetectRegressionTest.java
deleted file mode 100644
index 497d079..0000000
--- a/tests/src/com/android/tradefed/testtype/metricregression/DetectRegressionTest.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.metricregression;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.testtype.metricregression.DetectRegression.TableRow;
-import com.android.tradefed.util.MultiMap;
-
-import com.google.common.primitives.Doubles;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/** Unit tests for {@link DetectRegression}. */
-@RunWith(JUnit4.class)
-public class DetectRegressionTest {
-
-    private static final double EPS = 0.0001;
-
-    @Test
-    public void testCalcMean() {
-        Map<Double, double[]> data = new HashMap<>();
-        data.put(2.5, new double[] {1, 2, 3, 4});
-        data.put(
-                4.5,
-                new double[] {
-                    -11, 22, 5, 4, 2.5,
-                });
-        data.forEach(
-                (k, v) -> {
-                    assertTrue(equal(k, DetectRegression.calcMean(Doubles.asList(v))));
-                });
-    }
-
-    @Test
-    public void testCalcStdDev() {
-        Map<Double, double[]> data = new HashMap<>();
-        data.put(36.331000536732, new double[] {12.3, 56.7, 45.6, 124, 56});
-        data.put(119.99906922093, new double[] {123.4, 22.5, 5.67, 4.56, 2.5, 333});
-        data.forEach(
-                (k, v) -> {
-                    assertTrue(equal(k, DetectRegression.calcStdDev(Doubles.asList(v))));
-                });
-    }
-
-    @Test
-    public void testDetectRegression() {
-        List<List<Double>> befores =
-                Arrays.stream(
-                                new double[][] {
-                                    {3, 3, 3, 3, 3},
-                                    {3, 5, 3, 5, 4},
-                                    {3, 4, 3, 4, 3},
-                                    {1, 2, 3, 2, 1},
-                                    {-1, -2, -3, 0, 1, 2, 3},
-                                    {5, 6, 5, 6, 6, 5, 7},
-                                })
-                        .map(Doubles::asList)
-                        .collect(Collectors.toList());
-        List<List<Double>> afters =
-                Arrays.stream(
-                                new double[][] {
-                                    {3, 3, 3, 3, 3},
-                                    {2, 3, 4, 5, 6},
-                                    {2, 5, 2, 5, 2},
-                                    {10, 11, 12, 13, 14},
-                                    {-10, -20, -30, 0, 10, 20, 30},
-                                    {5, 6, 5, 6, 6, 5, 700},
-                                })
-                        .map(Doubles::asList)
-                        .collect(Collectors.toList());
-        boolean[] result = {false, false, true, true, true, false};
-
-        for (int i = 0; i < result.length; i++) {
-            assertEquals(
-                    result[i], DetectRegression.computeRegression(befores.get(i), afters.get(i)));
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    @Test
-    public void testRunRegressionDetection() {
-        DetectRegression detector = spy(DetectRegression.class);
-        doNothing().when(detector).logResult(any(), any(), any(), any());
-        TestDescription id1 = new TestDescription("class", "test1");
-        TestDescription id2 = new TestDescription("class", "test2");
-        Metrics before = new Metrics(false);
-        Arrays.asList("3.0", "3.0", "3.0", "3.0", "3.0")
-                .forEach(e -> before.addRunMetric("metric-1", e));
-        Arrays.asList("3.1", "3.3", "3.1", "3.2", "3.3")
-                .forEach(e -> before.addRunMetric("metric-2", e));
-        Arrays.asList("5.1", "5.2", "5.1", "5.2", "5.1")
-                .forEach(e -> before.addRunMetric("metric-3", e));
-        Arrays.asList("3.0", "3.0", "3.0", "3.0", "3.0")
-                .forEach(e -> before.addTestMetric(id1, "metric-4", e));
-        Arrays.asList("3.1", "3.3", "3.1", "3.2", "3.3")
-                .forEach(e -> before.addTestMetric(id2, "metric-5", e));
-        Arrays.asList("5.1", "5.2", "5.1", "5.2", "5.1")
-                .forEach(e -> before.addTestMetric(id2, "metric-6", e));
-
-        Metrics after = new Metrics(false);
-        Arrays.asList("3.0", "3.0", "3.0", "3.0", "3.0")
-                .forEach(e -> after.addRunMetric("metric-1", e));
-        Arrays.asList("3.2", "3.2", "3.2", "3.2", "3.2")
-                .forEach(e -> after.addRunMetric("metric-2", e));
-        Arrays.asList("8.1", "8.2", "8.1", "8.2", "8.1")
-                .forEach(e -> after.addRunMetric("metric-3", e));
-        Arrays.asList("3.0", "3.0", "3.0", "3.0", "3.0")
-                .forEach(e -> after.addTestMetric(id1, "metric-4", e));
-        Arrays.asList("3.2", "3.2", "3.2", "3.2", "3.2")
-                .forEach(e -> after.addTestMetric(id2, "metric-5", e));
-        Arrays.asList("8.1", "8.2", "8.1", "8.2", "8.1")
-                .forEach(e -> after.addTestMetric(id2, "metric-6", e));
-
-        ArgumentCaptor<List<TableRow>> runResultCaptor = ArgumentCaptor.forClass(List.class);
-        ArgumentCaptor<MultiMap<String, TableRow>> testResultCaptor =
-                ArgumentCaptor.forClass(MultiMap.class);
-        detector.runRegressionDetection(before, after);
-        verify(detector, times(1))
-                .logResult(
-                        eq(before),
-                        eq(after),
-                        runResultCaptor.capture(),
-                        testResultCaptor.capture());
-
-        List<TableRow> runResults = runResultCaptor.getValue();
-        assertEquals(1, runResults.size());
-        assertEquals("metric-3", runResults.get(0).name);
-
-        MultiMap<String, TableRow> testResults = testResultCaptor.getValue();
-        assertEquals(1, testResults.size());
-        assertEquals(1, testResults.get(id2.toString()).size());
-        assertEquals("metric-6", testResults.get(id2.toString()).get(0).name);
-    }
-
-    private boolean equal(double d1, double d2) {
-        return Math.abs(d1 - d2) < EPS;
-    }
-}
diff --git a/tests/src/com/android/tradefed/testtype/metricregression/MetricsTest.java b/tests/src/com/android/tradefed/testtype/metricregression/MetricsTest.java
deleted file mode 100644
index 7238a67..0000000
--- a/tests/src/com/android/tradefed/testtype/metricregression/MetricsTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.tradefed.testtype.metricregression;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.util.Pair;
-
-import com.google.common.primitives.Doubles;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Unit tests for {@link Metrics}. */
-@RunWith(JUnit4.class)
-public class MetricsTest {
-
-    private Metrics mMetrics;
-
-    @Before
-    public void setUp() throws Exception {
-        mMetrics = spy(new Metrics(false));
-    }
-
-    @Test
-    public void testAddRunMetrics() {
-        Map<String, List<String>> data = new HashMap<>();
-        data.put("metric1", Arrays.asList("1.0", "1.1", "1.2"));
-        data.put("metric2", Arrays.asList("2.0", "2.1", "2.2"));
-        data.forEach((k, v) -> v.forEach(e -> mMetrics.addRunMetric(k, e)));
-        assertEquals(Doubles.asList(1.0, 1.1, 1.2), mMetrics.getRunMetrics().get("metric1"));
-        assertEquals(Doubles.asList(2.0, 2.1, 2.2), mMetrics.getRunMetrics().get("metric2"));
-    }
-
-    @Test
-    public void testAddTestMetrics() {
-        TestDescription id1 = new TestDescription("class", "test1");
-        Arrays.asList("1.0", "1.1", "1.2").forEach(e -> mMetrics.addTestMetric(id1, "metric1", e));
-        TestDescription id2 = new TestDescription("class", "test2");
-        Arrays.asList("2.0", "2.1", "2.2").forEach(e -> mMetrics.addTestMetric(id2, "metric1", e));
-        Arrays.asList("3.0", "3.1", "3.2").forEach(e -> mMetrics.addTestMetric(id2, "metric2", e));
-
-        assertEquals(
-                Doubles.asList(1.0, 1.1, 1.2),
-                mMetrics.getTestMetrics().get(new Pair<>(id1, "metric1")));
-        assertEquals(
-                Doubles.asList(2.0, 2.1, 2.2),
-                mMetrics.getTestMetrics().get(new Pair<>(id2, "metric1")));
-        assertEquals(
-                Doubles.asList(3.0, 3.1, 3.2),
-                mMetrics.getTestMetrics().get(new Pair<>(id2, "metric2")));
-    }
-
-    @Test
-    public void testValidate() {
-        Map<String, List<String>> data = new HashMap<>();
-        data.put("metric1", Arrays.asList("1.0", "1.1", "1.2"));
-        data.put("metric2", Arrays.asList("2.0", "2.1"));
-        data.forEach((k, v) -> v.forEach(e -> mMetrics.addRunMetric(k, e)));
-        TestDescription id1 = new TestDescription("class", "test1");
-        Arrays.asList("1.0", "1.1", "1.2").forEach(e -> mMetrics.addTestMetric(id1, "metric1", e));
-        TestDescription id2 = new TestDescription("class", "test2");
-        Arrays.asList("2.0", "2.1", "2.2").forEach(e -> mMetrics.addTestMetric(id2, "metric1", e));
-        Arrays.asList("3.0", "3.1").forEach(e -> mMetrics.addTestMetric(id2, "metric2", e));
-        mMetrics.validate(3);
-        verify(mMetrics, times(2)).error(anyString());
-    }
-
-    @Test
-    public void testCrossValidate() {
-        Metrics other = new Metrics(false);
-        Arrays.asList("1.0", "1.1", "1.2")
-                .forEach(
-                        e -> {
-                            mMetrics.addRunMetric("metric1", e);
-                            other.addRunMetric("metric1", e);
-                        });
-        Arrays.asList("2.0", "2.1", "2.2").forEach(e -> mMetrics.addRunMetric("metric2", e));
-        Arrays.asList("2.0", "2.1", "2.2").forEach(e -> other.addRunMetric("metric5", e));
-        TestDescription id1 = new TestDescription("class", "test1");
-        Arrays.asList("1.0", "1.1", "1.2")
-                .forEach(
-                        e -> {
-                            mMetrics.addTestMetric(id1, "metric1", e);
-                            other.addTestMetric(id1, "metric1", e);
-                        });
-        Arrays.asList("3.0", "3.1", "3.3").forEach(e -> mMetrics.addTestMetric(id1, "metric6", e));
-        TestDescription id2 = new TestDescription("class", "test2");
-        Arrays.asList("2.0", "2.1", "2.2")
-                .forEach(
-                        e -> {
-                            mMetrics.addTestMetric(id2, "metric1", e);
-                            other.addTestMetric(id2, "metric1", e);
-                        });
-        Arrays.asList("3.0", "3.1", "3.3").forEach(e -> other.addTestMetric(id2, "metric2", e));
-        mMetrics.crossValidate(other);
-        verify(mMetrics, times(1)).warn("Run metric \"metric2\" only in before-patch run.");
-        verify(mMetrics, times(1)).warn("Run metric \"metric5\" only in after-patch run.");
-        verify(mMetrics, times(1))
-                .warn(
-                        String.format(
-                                "Test %s metric \"metric6\" only in before-patch run.",
-                                id1.toString()));
-        verify(mMetrics, times(1))
-                .warn(
-                        String.format(
-                                "Test %s metric \"metric2\" only in after-patch run.",
-                                id2.toString()));
-    }
-}
diff --git a/tests/src/com/android/tradefed/util/MetricsXmlParserTest.java b/tests/src/com/android/tradefed/util/MetricsXmlParserTest.java
deleted file mode 100644
index 9cb20de..0000000
--- a/tests/src/com/android/tradefed/util/MetricsXmlParserTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.tradefed.util;
-
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import com.android.tradefed.build.BuildInfo;
-import com.android.tradefed.config.OptionSetter;
-import com.android.tradefed.invoker.IInvocationContext;
-import com.android.tradefed.invoker.InvocationContext;
-import com.android.tradefed.metrics.proto.MetricMeasurement.Metric;
-import com.android.tradefed.result.MetricsXMLResultReporter;
-import com.android.tradefed.result.TestDescription;
-import com.android.tradefed.testtype.metricregression.Metrics;
-import com.android.tradefed.util.MetricsXmlParser.ParseException;
-import com.android.tradefed.util.proto.TfMetricProtoUtil;
-
-import com.google.common.collect.ImmutableSet;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-/** Simple unit tests for {@link MetricsXmlParser}. */
-@RunWith(JUnit4.class)
-public class MetricsXmlParserTest {
-
-    @Spy private MetricsXMLResultReporter mResultReporter;
-    @Mock private Metrics mMetrics;
-    private ByteArrayOutputStream mOutputStream;
-
-    @Before
-    public void setUp() throws Exception {
-        mOutputStream = new ByteArrayOutputStream();
-        MockitoAnnotations.initMocks(this);
-        OptionSetter optionSetter = new OptionSetter(mResultReporter);
-        optionSetter.setOptionValue("metrics-folder", "/tmp");
-        doReturn(mOutputStream).when(mResultReporter).createOutputStream();
-        doReturn("ignore").when(mResultReporter).getTimeStamp();
-    }
-
-    /** Test behavior when data to parse is empty */
-    @Test
-    public void testEmptyParse() {
-        try {
-            MetricsXmlParser.parse(
-                    mMetrics, Collections.emptySet(), new ByteArrayInputStream(new byte[0]));
-            fail("ParseException not thrown");
-        } catch (ParseException e) {
-            // expected
-        }
-        Mockito.verifyZeroInteractions(mMetrics);
-    }
-
-    /** Simple success test for xml parsing */
-    @Test
-    public void testSimpleParse() throws ParseException {
-        IInvocationContext context = new InvocationContext();
-        context.addDeviceBuildInfo("fakeDevice", new BuildInfo());
-        context.setTestTag("stub");
-        mResultReporter.invocationStarted(context);
-        mResultReporter.testRunStarted("run", 3);
-        final TestDescription testId0 = new TestDescription("Test", "pass1");
-        mResultReporter.testStarted(testId0);
-        mResultReporter.testEnded(testId0, new HashMap<String, Metric>());
-        final TestDescription testId1 = new TestDescription("Test", "pass2");
-        mResultReporter.testStarted(testId1);
-        mResultReporter.testEnded(testId1, new HashMap<String, Metric>());
-        final TestDescription testId2 = new TestDescription("Test", "pass3");
-        mResultReporter.testStarted(testId2);
-        mResultReporter.testEnded(testId2, new HashMap<String, Metric>());
-        mResultReporter.testRunEnded(3, new HashMap<String, Metric>());
-        mResultReporter.invocationEnded(5);
-
-        MetricsXmlParser.parse(
-                mMetrics, Collections.emptySet(), new ByteArrayInputStream(getOutput()));
-        verify(mMetrics).setNumTests(3);
-        verify(mMetrics).addRunMetric("time", "5");
-        verify(mMetrics, times(0)).addTestMetric(any(), anyString(), anyString());
-        Mockito.verifyNoMoreInteractions(mMetrics);
-    }
-
-    /** Test parsing a comprehensive document containing run metrics and test metrics */
-    @Test
-    public void testParse() throws ParseException {
-        IInvocationContext context = new InvocationContext();
-        context.addDeviceBuildInfo("fakeDevice", new BuildInfo());
-        context.setTestTag("stub");
-        mResultReporter.invocationStarted(context);
-        mResultReporter.testRunStarted("run", 2);
-
-        final TestDescription testId0 = new TestDescription("Test", "pass1");
-        mResultReporter.testStarted(testId0);
-        Map<String, String> testMetrics0 = new HashMap<>();
-        testMetrics0.put("metric1", "1.1");
-        mResultReporter.testEnded(testId0, TfMetricProtoUtil.upgradeConvert(testMetrics0));
-
-        final TestDescription testId1 = new TestDescription("Test", "pass2");
-        mResultReporter.testStarted(testId1);
-        Map<String, String> testMetrics1 = new HashMap<>();
-        testMetrics1.put("metric2", "5.5");
-        mResultReporter.testEnded(testId1, TfMetricProtoUtil.upgradeConvert(testMetrics1));
-
-        Map<String, String> runMetrics = new HashMap<>();
-        runMetrics.put("metric3", "8.8");
-        mResultReporter.testRunEnded(3, TfMetricProtoUtil.upgradeConvert(runMetrics));
-        mResultReporter.invocationEnded(5);
-
-        MetricsXmlParser.parse(
-                mMetrics, Collections.emptySet(), new ByteArrayInputStream(getOutput()));
-
-        verify(mMetrics).setNumTests(2);
-        verify(mMetrics).addRunMetric("metric3", "8.8");
-        verify(mMetrics).addTestMetric(testId0, "metric1", "1.1");
-        verify(mMetrics).addTestMetric(testId1, "metric2", "5.5");
-    }
-
-    /** Test parsing a document with blacklist metrics */
-    @Test
-    public void testParseBlacklist() throws ParseException {
-        IInvocationContext context = new InvocationContext();
-        context.addDeviceBuildInfo("fakeDevice", new BuildInfo());
-        context.setTestTag("stub");
-        mResultReporter.invocationStarted(context);
-        mResultReporter.testRunStarted("run", 3);
-
-        final TestDescription testId0 = new TestDescription("Test", "pass1");
-        mResultReporter.testStarted(testId0);
-        Map<String, String> testMetrics0 = new HashMap<>();
-        testMetrics0.put("metric1", "1.1");
-        mResultReporter.testEnded(testId0, TfMetricProtoUtil.upgradeConvert(testMetrics0));
-
-        final TestDescription testId1 = new TestDescription("Test", "pass2");
-        mResultReporter.testStarted(testId1);
-        Map<String, String> testMetrics1 = new HashMap<>();
-        testMetrics1.put("metric2", "5.5");
-        mResultReporter.testEnded(testId1, TfMetricProtoUtil.upgradeConvert(testMetrics1));
-
-        Map<String, String> runMetrics = new HashMap<>();
-        runMetrics.put("metric3", "8.8");
-        mResultReporter.testRunEnded(3, TfMetricProtoUtil.upgradeConvert(runMetrics));
-        mResultReporter.invocationEnded(5);
-
-        Set<String> blacklist = ImmutableSet.of("metric1", "metric3");
-
-        MetricsXmlParser.parse(mMetrics, blacklist, new ByteArrayInputStream(getOutput()));
-
-        verify(mMetrics, times(0)).addRunMetric("metric3", "8.8");
-        verify(mMetrics, times(0)).addTestMetric(testId0, "metric1", "1.1");
-        verify(mMetrics).addTestMetric(testId1, "metric2", "5.5");
-    }
-
-    /** Gets the output produced, stripping it of extraneous whitespace characters. */
-    private byte[] getOutput() {
-        return mOutputStream.toByteArray();
-    }
-}
diff --git a/tests/src/com/android/tradefed/util/RunUtilTest.java b/tests/src/com/android/tradefed/util/RunUtilTest.java
index 7403912..4626b85 100644
--- a/tests/src/com/android/tradefed/util/RunUtilTest.java
+++ b/tests/src/com/android/tradefed/util/RunUtilTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
@@ -43,6 +44,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.concurrent.TimeUnit;
 
 /** Unit tests for {@link RunUtil} */
 @RunWith(JUnit4.class)
@@ -426,46 +428,16 @@
      */
     @Test
     public void testSetInterruptibleInFuture() {
-        final Thread test =
-                new Thread(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                mRunUtil.allowInterrupt(false);
-                                assertFalse(mRunUtil.isInterruptAllowed());
-                                mRunUtil.setInterruptibleInFuture(Thread.currentThread(), 10);
-                                try {
-                                    mRunUtil.sleep(SHORT_TIMEOUT_MS);
-                                    mRunUtil.sleep(SHORT_TIMEOUT_MS);
-                                    fail();
-                                } catch (RunInterruptedException rie) {
-                                    assertEquals("TEST", rie.getMessage());
-                                }
-                                success = mRunUtil.isInterruptAllowed();
-                            }
-                        });
-        mRunUtil.interrupt(test, "TEST");
-        test.start();
-        try {
-            test.join();
-        } catch (InterruptedException e) {
-            // Ignore
-        }
-        assertTrue(success);
-    }
+        CommandInterrupter interrupter = Mockito.mock(CommandInterrupter.class);
+        RunUtil runUtil = new RunUtil(interrupter);
 
-    /** Test whether a {@link RunUtil#setInterruptibleInFuture} has not change the state yet. */
-    @Test
-    public void testSetInterruptibleInFuture_beforeTimeout() {
-        mRunUtil.allowInterrupt(false);
-        assertFalse(mRunUtil.isInterruptAllowed());
+        Thread thread = new Thread();
+        runUtil.setInterruptibleInFuture(thread, 123L);
 
-        mRunUtil.setInterruptibleInFuture(Thread.currentThread(), SHORT_TIMEOUT_MS);
-        mRunUtil.sleep(50);
-        // Should still be false
-        assertFalse(mRunUtil.isInterruptAllowed());
-        mRunUtil.sleep(LONG_TIMEOUT_MS);
-        assertTrue(mRunUtil.isInterruptAllowed());
+        // RunUtil delegates to CommandInterrupter#allowInterruptAsync
+        Mockito.verify(interrupter)
+                .allowInterruptAsync(eq(thread), eq(123L), eq(TimeUnit.MILLISECONDS));
+        Mockito.verifyNoMoreInteractions(interrupter);
     }
 
     /** Test {@link RunUtil#setEnvVariablePriority(EnvPriority)} properly prioritize unset. */