Import Cobalt 21.master.0.301702
diff --git a/src/build/android/pylib/base/__init__.py b/src/build/android/pylib/base/__init__.py
new file mode 100644
index 0000000..96196cf
--- /dev/null
+++ b/src/build/android/pylib/base/__init__.py
@@ -0,0 +1,3 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
diff --git a/src/build/android/pylib/base/base_test_result.py b/src/build/android/pylib/base/base_test_result.py
new file mode 100644
index 0000000..03f00f2
--- /dev/null
+++ b/src/build/android/pylib/base/base_test_result.py
@@ -0,0 +1,264 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Module containing base test results classes."""
+
+from __future__ import absolute_import
+import threading
+import six
+
+
+class ResultType(object):
+  """Class enumerating test types."""
+  # The test passed.
+  PASS = 'SUCCESS'
+
+  # The test was intentionally skipped.
+  SKIP = 'SKIPPED'
+
+  # The test failed.
+  FAIL = 'FAILURE'
+
+  # The test caused the containing process to crash.
+  CRASH = 'CRASH'
+
+  # The test timed out.
+  TIMEOUT = 'TIMEOUT'
+
+  # The test ran, but we couldn't determine what happened.
+  UNKNOWN = 'UNKNOWN'
+
+  # The test did not run.
+  NOTRUN = 'NOTRUN'
+
+  @staticmethod
+  def GetTypes():
+    """Get a list of all test types."""
+    return [ResultType.PASS, ResultType.SKIP, ResultType.FAIL,
+            ResultType.CRASH, ResultType.TIMEOUT, ResultType.UNKNOWN,
+            ResultType.NOTRUN]
+
+
+class BaseTestResult(object):
+  """Base class for a single test result."""
+
+  def __init__(self, name, test_type, duration=0, log=''):
+    """Construct a BaseTestResult.
+
+    Args:
+      name: Name of the test which defines uniqueness.
+      test_type: Type of the test result as defined in ResultType.
+      duration: Time it took for the test to run in milliseconds.
+      log: An optional string listing any errors.
+    """
+    assert name
+    assert test_type in ResultType.GetTypes()
+    self._name = name
+    self._test_type = test_type
+    self._duration = duration
+    self._log = log
+    self._links = {}
+
+  def __str__(self):
+    return self._name
+
+  def __repr__(self):
+    return self._name
+
+  def __cmp__(self, other):
+    # pylint: disable=W0212
+    return cmp(self._name, other._name)
+
+  def __hash__(self):
+    return hash(self._name)
+
+  def SetName(self, name):
+    """Set the test name.
+
+    Because we're putting this into a set, this should only be used if moving
+    this test result into another set.
+    """
+    self._name = name
+
+  def GetName(self):
+    """Get the test name."""
+    return self._name
+
+  def SetType(self, test_type):
+    """Set the test result type."""
+    assert test_type in ResultType.GetTypes()
+    self._test_type = test_type
+
+  def GetType(self):
+    """Get the test result type."""
+    return self._test_type
+
+  def GetDuration(self):
+    """Get the test duration."""
+    return self._duration
+
+  def SetLog(self, log):
+    """Set the test log."""
+    self._log = log
+
+  def GetLog(self):
+    """Get the test log."""
+    return self._log
+
+  def SetLink(self, name, link_url):
+    """Set link with test result data."""
+    self._links[name] = link_url
+
+  def GetLinks(self):
+    """Get dict containing links to test result data."""
+    return self._links
+
+
+class TestRunResults(object):
+  """Set of results for a test run."""
+
+  def __init__(self):
+    self._links = {}
+    self._results = set()
+    self._results_lock = threading.RLock()
+
+  def SetLink(self, name, link_url):
+    """Add link with test run results data."""
+    self._links[name] = link_url
+
+  def GetLinks(self):
+    """Get dict containing links to test run result data."""
+    return self._links
+
+  def GetLogs(self):
+    """Get the string representation of all test logs."""
+    with self._results_lock:
+      s = []
+      for test_type in ResultType.GetTypes():
+        if test_type != ResultType.PASS:
+          for t in sorted(self._GetType(test_type)):
+            log = t.GetLog()
+            if log:
+              s.append('[%s] %s:' % (test_type, t))
+              s.append(six.text_type(log, 'utf-8'))
+      return '\n'.join(s)
+
+  def GetGtestForm(self):
+    """Get the gtest string representation of this object."""
+    with self._results_lock:
+      s = []
+      plural = lambda n, s, p: '%d %s' % (n, p if n != 1 else s)
+      tests = lambda n: plural(n, 'test', 'tests')
+
+      s.append('[==========] %s ran.' % (tests(len(self.GetAll()))))
+      s.append('[  PASSED  ] %s.' % (tests(len(self.GetPass()))))
+
+      skipped = self.GetSkip()
+      if skipped:
+        s.append('[  SKIPPED ] Skipped %s, listed below:' % tests(len(skipped)))
+        for t in sorted(skipped):
+          s.append('[  SKIPPED ] %s' % str(t))
+
+      all_failures = self.GetFail().union(self.GetCrash(), self.GetTimeout(),
+          self.GetUnknown())
+      if all_failures:
+        s.append('[  FAILED  ] %s, listed below:' % tests(len(all_failures)))
+        for t in sorted(self.GetFail()):
+          s.append('[  FAILED  ] %s' % str(t))
+        for t in sorted(self.GetCrash()):
+          s.append('[  FAILED  ] %s (CRASHED)' % str(t))
+        for t in sorted(self.GetTimeout()):
+          s.append('[  FAILED  ] %s (TIMEOUT)' % str(t))
+        for t in sorted(self.GetUnknown()):
+          s.append('[  FAILED  ] %s (UNKNOWN)' % str(t))
+        s.append('')
+        s.append(plural(len(all_failures), 'FAILED TEST', 'FAILED TESTS'))
+      return '\n'.join(s)
+
+  def GetShortForm(self):
+    """Get the short string representation of this object."""
+    with self._results_lock:
+      s = []
+      s.append('ALL: %d' % len(self._results))
+      for test_type in ResultType.GetTypes():
+        s.append('%s: %d' % (test_type, len(self._GetType(test_type))))
+      return ''.join([x.ljust(15) for x in s])
+
+  def __str__(self):
+    return self.GetGtestForm()
+
+  def AddResult(self, result):
+    """Add |result| to the set.
+
+    Args:
+      result: An instance of BaseTestResult.
+    """
+    assert isinstance(result, BaseTestResult)
+    with self._results_lock:
+      self._results.discard(result)
+      self._results.add(result)
+
+  def AddResults(self, results):
+    """Add |results| to the set.
+
+    Args:
+      results: An iterable of BaseTestResult objects.
+    """
+    with self._results_lock:
+      for t in results:
+        self.AddResult(t)
+
+  def AddTestRunResults(self, results):
+    """Add the set of test results from |results|.
+
+    Args:
+      results: An instance of TestRunResults.
+    """
+    assert isinstance(results, TestRunResults), (
+           'Expected TestRunResult object: %s' % type(results))
+    with self._results_lock:
+      # pylint: disable=W0212
+      self._results.update(results._results)
+
+  def GetAll(self):
+    """Get the set of all test results."""
+    with self._results_lock:
+      return self._results.copy()
+
+  def _GetType(self, test_type):
+    """Get the set of test results with the given test type."""
+    with self._results_lock:
+      return set(t for t in self._results if t.GetType() == test_type)
+
+  def GetPass(self):
+    """Get the set of all passed test results."""
+    return self._GetType(ResultType.PASS)
+
+  def GetSkip(self):
+    """Get the set of all skipped test results."""
+    return self._GetType(ResultType.SKIP)
+
+  def GetFail(self):
+    """Get the set of all failed test results."""
+    return self._GetType(ResultType.FAIL)
+
+  def GetCrash(self):
+    """Get the set of all crashed test results."""
+    return self._GetType(ResultType.CRASH)
+
+  def GetTimeout(self):
+    """Get the set of all timed out test results."""
+    return self._GetType(ResultType.TIMEOUT)
+
+  def GetUnknown(self):
+    """Get the set of all unknown test results."""
+    return self._GetType(ResultType.UNKNOWN)
+
+  def GetNotPass(self):
+    """Get the set of all non-passed test results."""
+    return self.GetAll() - self.GetPass()
+
+  def DidRunPass(self):
+    """Return whether the test run was successful."""
+    return not self.GetNotPass() - self.GetSkip()
diff --git a/src/build/android/pylib/base/base_test_result_unittest.py b/src/build/android/pylib/base/base_test_result_unittest.py
new file mode 100644
index 0000000..31a1f60
--- /dev/null
+++ b/src/build/android/pylib/base/base_test_result_unittest.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittests for TestRunResults."""
+
+from __future__ import absolute_import
+import unittest
+
+from pylib.base.base_test_result import BaseTestResult
+from pylib.base.base_test_result import TestRunResults
+from pylib.base.base_test_result import ResultType
+
+
+class TestTestRunResults(unittest.TestCase):
+  def setUp(self):
+    self.p1 = BaseTestResult('p1', ResultType.PASS, log='pass1')
+    other_p1 = BaseTestResult('p1', ResultType.PASS)
+    self.p2 = BaseTestResult('p2', ResultType.PASS)
+    self.f1 = BaseTestResult('f1', ResultType.FAIL, log='failure1')
+    self.c1 = BaseTestResult('c1', ResultType.CRASH, log='crash1')
+    self.u1 = BaseTestResult('u1', ResultType.UNKNOWN)
+    self.tr = TestRunResults()
+    self.tr.AddResult(self.p1)
+    self.tr.AddResult(other_p1)
+    self.tr.AddResult(self.p2)
+    self.tr.AddResults(set([self.f1, self.c1, self.u1]))
+
+  def testGetAll(self):
+    self.assertFalse(
+        self.tr.GetAll().symmetric_difference(
+            [self.p1, self.p2, self.f1, self.c1, self.u1]))
+
+  def testGetPass(self):
+    self.assertFalse(self.tr.GetPass().symmetric_difference(
+        [self.p1, self.p2]))
+
+  def testGetNotPass(self):
+    self.assertFalse(self.tr.GetNotPass().symmetric_difference(
+        [self.f1, self.c1, self.u1]))
+
+  def testGetAddTestRunResults(self):
+    tr2 = TestRunResults()
+    other_p1 = BaseTestResult('p1', ResultType.PASS)
+    f2 = BaseTestResult('f2', ResultType.FAIL)
+    tr2.AddResult(other_p1)
+    tr2.AddResult(f2)
+    tr2.AddTestRunResults(self.tr)
+    self.assertFalse(
+        tr2.GetAll().symmetric_difference(
+            [self.p1, self.p2, self.f1, self.c1, self.u1, f2]))
+
+  def testGetLogs(self):
+    log_print = ('[FAIL] f1:\n'
+                 'failure1\n'
+                 '[CRASH] c1:\n'
+                 'crash1')
+    self.assertEqual(self.tr.GetLogs(), log_print)
+
+  def testGetShortForm(self):
+    short_print = ('ALL: 5         PASS: 2        FAIL: 1        '
+                   'CRASH: 1       TIMEOUT: 0     UNKNOWN: 1     ')
+    self.assertEqual(self.tr.GetShortForm(), short_print)
+
+  def testGetGtestForm(self):
+    gtest_print = ('[==========] 5 tests ran.\n'
+                   '[  PASSED  ] 2 tests.\n'
+                   '[  FAILED  ] 3 tests, listed below:\n'
+                   '[  FAILED  ] f1\n'
+                   '[  FAILED  ] c1 (CRASHED)\n'
+                   '[  FAILED  ] u1 (UNKNOWN)\n'
+                   '\n'
+                   '3 FAILED TESTS')
+    self.assertEqual(gtest_print, self.tr.GetGtestForm())
+
+  def testRunPassed(self):
+    self.assertFalse(self.tr.DidRunPass())
+    tr2 = TestRunResults()
+    self.assertTrue(tr2.DidRunPass())
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/src/build/android/pylib/base/environment.py b/src/build/android/pylib/base/environment.py
new file mode 100644
index 0000000..744c392
--- /dev/null
+++ b/src/build/android/pylib/base/environment.py
@@ -0,0 +1,49 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+class Environment(object):
+  """An environment in which tests can be run.
+
+  This is expected to handle all logic that is applicable to an entire specific
+  environment but is independent of the test type.
+
+  Examples include:
+    - The local device environment, for running tests on devices attached to
+      the local machine.
+    - The local machine environment, for running tests directly on the local
+      machine.
+  """
+
+  def __init__(self, output_manager):
+    """Environment constructor.
+
+    Args:
+      output_manager: Instance of |output_manager.OutputManager| used to
+          save test output.
+    """
+    self._output_manager = output_manager
+
+    # Some subclasses have different teardown behavior on receiving SIGTERM.
+    self._received_sigterm = False
+
+  def SetUp(self):
+    raise NotImplementedError
+
+  def TearDown(self):
+    raise NotImplementedError
+
+  def __enter__(self):
+    self.SetUp()
+    return self
+
+  def __exit__(self, _exc_type, _exc_val, _exc_tb):
+    self.TearDown()
+
+  @property
+  def output_manager(self):
+    return self._output_manager
+
+  def ReceivedSigterm(self):
+    self._received_sigterm = True
diff --git a/src/build/android/pylib/base/environment_factory.py b/src/build/android/pylib/base/environment_factory.py
new file mode 100644
index 0000000..2ff93f3
--- /dev/null
+++ b/src/build/android/pylib/base/environment_factory.py
@@ -0,0 +1,34 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+from pylib import constants
+from pylib.local.device import local_device_environment
+from pylib.local.machine import local_machine_environment
+
+try:
+  # local_emulator_environment depends on //tools.
+  # If a client pulls in the //build subtree but not the //tools
+  # one, fail at emulator environment creation time.
+  from pylib.local.emulator import local_emulator_environment
+except ImportError:
+  local_emulator_environment = None
+
+
+def CreateEnvironment(args, output_manager, error_func):
+
+  if args.environment == 'local':
+    if args.command not in constants.LOCAL_MACHINE_TESTS:
+      if args.avd_config:
+        if not local_emulator_environment:
+          error_func('emulator environment requested but not available.')
+        return local_emulator_environment.LocalEmulatorEnvironment(
+            args, output_manager, error_func)
+      return local_device_environment.LocalDeviceEnvironment(
+          args, output_manager, error_func)
+    else:
+      return local_machine_environment.LocalMachineEnvironment(
+          args, output_manager, error_func)
+
+  error_func('Unable to create %s environment.' % args.environment)
diff --git a/src/build/android/pylib/base/mock_environment.py b/src/build/android/pylib/base/mock_environment.py
new file mode 100644
index 0000000..d7293c7
--- /dev/null
+++ b/src/build/android/pylib/base/mock_environment.py
@@ -0,0 +1,11 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+from pylib.base import environment
+
+import mock  # pylint: disable=import-error
+
+
+MockEnvironment = mock.MagicMock(environment.Environment)
diff --git a/src/build/android/pylib/base/mock_test_instance.py b/src/build/android/pylib/base/mock_test_instance.py
new file mode 100644
index 0000000..19a1d7e
--- /dev/null
+++ b/src/build/android/pylib/base/mock_test_instance.py
@@ -0,0 +1,11 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+from pylib.base import test_instance
+
+import mock  # pylint: disable=import-error
+
+
+MockTestInstance = mock.MagicMock(test_instance.TestInstance)
diff --git a/src/build/android/pylib/base/output_manager.py b/src/build/android/pylib/base/output_manager.py
new file mode 100644
index 0000000..53e5aea
--- /dev/null
+++ b/src/build/android/pylib/base/output_manager.py
@@ -0,0 +1,159 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+import contextlib
+import logging
+import os
+import tempfile
+
+from devil.utils import reraiser_thread
+
+
+class Datatype(object):
+  HTML = 'text/html'
+  JSON = 'application/json'
+  PNG = 'image/png'
+  TEXT = 'text/plain'
+
+
+class OutputManager(object):
+
+  def __init__(self):
+    """OutputManager Constructor.
+
+    This class provides a simple interface to save test output. Subclasses
+    of this will allow users to save test results in the cloud or locally.
+    """
+    self._allow_upload = False
+    self._thread_group = None
+
+  @contextlib.contextmanager
+  def ArchivedTempfile(
+      self, out_filename, out_subdir, datatype=Datatype.TEXT):
+    """Archive file contents asynchonously and then deletes file.
+
+    Args:
+      out_filename: Name for saved file.
+      out_subdir: Directory to save |out_filename| to.
+      datatype: Datatype of file.
+
+    Returns:
+      An ArchivedFile file. This file will be uploaded async when the context
+      manager exits. AFTER the context manager exits, you can get the link to
+      where the file will be stored using the Link() API. You can use typical
+      file APIs to write and flish the ArchivedFile. You can also use file.name
+      to get the local filepath to where the underlying file exists. If you do
+      this, you are responsible of flushing the file before exiting the context
+      manager.
+    """
+    if not self._allow_upload:
+      raise Exception('Must run |SetUp| before attempting to upload!')
+
+    f = self._CreateArchivedFile(out_filename, out_subdir, datatype)
+    try:
+      yield f
+    finally:
+      f.PrepareArchive()
+
+      def archive():
+        try:
+          f.Archive()
+        finally:
+          f.Delete()
+
+      thread = reraiser_thread.ReraiserThread(func=archive)
+      thread.start()
+      self._thread_group.Add(thread)
+
+  def _CreateArchivedFile(self, out_filename, out_subdir, datatype):
+    """Returns an instance of ArchivedFile."""
+    raise NotImplementedError
+
+  def SetUp(self):
+    self._allow_upload = True
+    self._thread_group = reraiser_thread.ReraiserThreadGroup()
+
+  def TearDown(self):
+    self._allow_upload = False
+    logging.info('Finishing archiving output.')
+    self._thread_group.JoinAll()
+
+  def __enter__(self):
+    self.SetUp()
+    return self
+
+  def __exit__(self, _exc_type, _exc_val, _exc_tb):
+    self.TearDown()
+
+
+class ArchivedFile(object):
+
+  def __init__(self, out_filename, out_subdir, datatype):
+    self._out_filename = out_filename
+    self._out_subdir = out_subdir
+    self._datatype = datatype
+
+    self._f = tempfile.NamedTemporaryFile(delete=False)
+    self._ready_to_archive = False
+
+  @property
+  def name(self):
+    return self._f.name
+
+  def write(self, *args, **kwargs):
+    if self._ready_to_archive:
+      raise Exception('Cannot write to file after archiving has begun!')
+    self._f.write(*args, **kwargs)
+
+  def flush(self, *args, **kwargs):
+    if self._ready_to_archive:
+      raise Exception('Cannot flush file after archiving has begun!')
+    self._f.flush(*args, **kwargs)
+
+  def Link(self):
+    """Returns location of archived file."""
+    if not self._ready_to_archive:
+      raise Exception('Cannot get link to archived file before archiving '
+                      'has begun')
+    return self._Link()
+
+  def _Link(self):
+    """Note for when overriding this function.
+
+    This function will certainly be called before the file
+    has finished being archived. Therefore, this needs to be able to know the
+    exact location of the archived file before it is finished being archived.
+    """
+    raise NotImplementedError
+
+  def PrepareArchive(self):
+    """Meant to be called synchronously to prepare file for async archiving."""
+    self.flush()
+    self._ready_to_archive = True
+    self._PrepareArchive()
+
+  def _PrepareArchive(self):
+    """Note for when overriding this function.
+
+    This function is needed for things such as computing the location of
+    content addressed files. This is called after the file is written but
+    before archiving has begun.
+    """
+    pass
+
+  def Archive(self):
+    """Archives file."""
+    if not self._ready_to_archive:
+      raise Exception('File is not ready to archive. Be sure you are not '
+                      'writing to the file and PrepareArchive has been called')
+    self._Archive()
+
+  def _Archive(self):
+    raise NotImplementedError
+
+  def Delete(self):
+    """Deletes the backing file."""
+    self._f.close()
+    os.remove(self.name)
diff --git a/src/build/android/pylib/base/output_manager_factory.py b/src/build/android/pylib/base/output_manager_factory.py
new file mode 100644
index 0000000..891692d
--- /dev/null
+++ b/src/build/android/pylib/base/output_manager_factory.py
@@ -0,0 +1,18 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+from pylib import constants
+from pylib.output import local_output_manager
+from pylib.output import remote_output_manager
+from pylib.utils import local_utils
+
+
+def CreateOutputManager(args):
+  if args.local_output or not local_utils.IsOnSwarming():
+    return local_output_manager.LocalOutputManager(
+        output_dir=constants.GetOutDirectory())
+  else:
+    return remote_output_manager.RemoteOutputManager(
+        bucket=args.gs_results_bucket)
diff --git a/src/build/android/pylib/base/output_manager_test_case.py b/src/build/android/pylib/base/output_manager_test_case.py
new file mode 100644
index 0000000..7b7e462
--- /dev/null
+++ b/src/build/android/pylib/base/output_manager_test_case.py
@@ -0,0 +1,15 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+import os.path
+import unittest
+
+
+class OutputManagerTestCase(unittest.TestCase):
+
+  def assertUsableTempFile(self, archived_tempfile):
+    self.assertTrue(bool(archived_tempfile.name))
+    self.assertTrue(os.path.exists(archived_tempfile.name))
+    self.assertTrue(os.path.isfile(archived_tempfile.name))
diff --git a/src/build/android/pylib/base/result_sink.py b/src/build/android/pylib/base/result_sink.py
new file mode 100644
index 0000000..424b873
--- /dev/null
+++ b/src/build/android/pylib/base/result_sink.py
@@ -0,0 +1,163 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+from __future__ import absolute_import
+import base64
+import cgi
+import json
+import os
+
+import six
+if not six.PY2:
+  import html  # pylint: disable=import-error
+
+from pylib.base import base_test_result
+import requests  # pylint: disable=import-error
+
+# Comes from luci/resultdb/pbutil/test_result.go
+MAX_REPORT_LEN = 4 * 1024
+
+# Maps base_test_results to the luci test-result.proto.
+# https://godoc.org/go.chromium.org/luci/resultdb/proto/v1#TestStatus
+RESULT_MAP = {
+    base_test_result.ResultType.UNKNOWN: 'ABORT',
+    base_test_result.ResultType.PASS: 'PASS',
+    base_test_result.ResultType.FAIL: 'FAIL',
+    base_test_result.ResultType.CRASH: 'CRASH',
+    base_test_result.ResultType.TIMEOUT: 'ABORT',
+    base_test_result.ResultType.SKIP: 'SKIP',
+    base_test_result.ResultType.NOTRUN: 'SKIP',
+}
+
+
+def TryInitClient():
+  """Tries to initialize a result_sink_client object.
+
+  Assumes that rdb stream is already running.
+
+  Returns:
+    A ResultSinkClient for the result_sink server else returns None.
+  """
+  try:
+    with open(os.environ['LUCI_CONTEXT']) as f:
+      sink = json.load(f)['result_sink']
+      return ResultSinkClient(sink)
+  except KeyError:
+    return None
+
+
+class ResultSinkClient(object):
+  """A class to store the sink's post configurations and make post requests.
+
+  This assumes that the rdb stream has been called already and that the
+  server is listening.
+  """
+  def __init__(self, context):
+    base_url = 'http://%s/prpc/luci.resultsink.v1.Sink' % context['address']
+    self.test_results_url = base_url + '/ReportTestResults'
+    self.report_artifacts_url = base_url + '/ReportInvocationLevelArtifacts'
+
+    self.headers = {
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+        'Authorization': 'ResultSink %s' % context['auth_token'],
+    }
+
+  def Post(self, test_id, status, duration, test_log, test_file,
+           artifacts=None):
+    """Uploads the test result to the ResultSink server.
+
+    This assumes that the rdb stream has been called already and that
+    server is ready listening.
+
+    Args:
+      test_id: A string representing the test's name.
+      status: A string representing if the test passed, failed, etc...
+      duration: An int representing time in ms.
+      test_log: A string representing the test's output.
+      test_file: A string representing the file location of the test.
+      artifacts: An optional dict of artifacts to attach to the test.
+
+    Returns:
+      N/A
+    """
+    assert status in RESULT_MAP
+    expected = status in (base_test_result.ResultType.PASS,
+                          base_test_result.ResultType.SKIP)
+    result_db_status = RESULT_MAP[status]
+
+    # Slightly smaller to allow addition of <pre> tags and message.
+    report_check_size = MAX_REPORT_LEN - 45
+    if six.PY2:
+      test_log_escaped = cgi.escape(test_log)
+    else:
+      test_log_escaped = html.escape(test_log)
+    if len(test_log_escaped) > report_check_size:
+      test_log_formatted = ('<pre>' + test_log_escaped[:report_check_size] +
+                            '...Full output in Artifact.</pre>')
+    else:
+      test_log_formatted = '<pre>' + test_log_escaped + '</pre>'
+
+    tr = {
+        'expected':
+        expected,
+        'status':
+        result_db_status,
+        'summaryHtml':
+        test_log_formatted,
+        'tags': [
+            {
+                'key': 'test_name',
+                'value': test_id,
+            },
+            {
+                # Status before getting mapped to result_db statuses.
+                'key': 'android_test_runner_status',
+                'value': status,
+            }
+        ],
+        'testId':
+        test_id,
+    }
+    artifacts = artifacts or {}
+    if len(test_log_escaped) > report_check_size:
+      # Upload the original log without any modifications.
+      b64_log = six.ensure_str(base64.b64encode(six.ensure_binary(test_log)))
+      artifacts.update({'Test Log': {'contents': b64_log}})
+    if artifacts:
+      tr['artifacts'] = artifacts
+
+    if duration is not None:
+      # Duration must be formatted to avoid scientific notation in case
+      # number is too small or too large. Result_db takes seconds, not ms.
+      # Need to use float() otherwise it does substitution first then divides.
+      tr['duration'] = '%.9fs' % float(duration / 1000.0)
+
+    if test_file and str(test_file).startswith('//'):
+      tr['testMetadata'] = {
+          'name': test_id,
+          'location': {
+              'file_name': test_file,
+              'repo': 'https://chromium.googlesource.com/chromium/src',
+          }
+      }
+
+    res = requests.post(url=self.test_results_url,
+                        headers=self.headers,
+                        data=json.dumps({'testResults': [tr]}))
+    res.raise_for_status()
+
+  def ReportInvocationLevelArtifacts(self, artifacts):
+    """Uploads invocation-level artifacts to the ResultSink server.
+
+    This is for artifacts that don't apply to a single test but to the test
+    invocation as a whole (eg: system logs).
+
+    Args:
+      artifacts: A dict of artifacts to attach to the invocation.
+    """
+    req = {'artifacts': artifacts}
+    res = requests.post(url=self.report_artifacts_url,
+                        headers=self.headers,
+                        data=json.dumps(req))
+    res.raise_for_status()
diff --git a/src/build/android/pylib/base/test_collection.py b/src/build/android/pylib/base/test_collection.py
new file mode 100644
index 0000000..83b3bf8
--- /dev/null
+++ b/src/build/android/pylib/base/test_collection.py
@@ -0,0 +1,81 @@
+# Copyright 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+import threading
+
+class TestCollection(object):
+  """A threadsafe collection of tests.
+
+  Args:
+    tests: List of tests to put in the collection.
+  """
+
+  def __init__(self, tests=None):
+    if not tests:
+      tests = []
+    self._lock = threading.Lock()
+    self._tests = []
+    self._tests_in_progress = 0
+    # Used to signal that an item is available or all items have been handled.
+    self._item_available_or_all_done = threading.Event()
+    for t in tests:
+      self.add(t)
+
+  def _pop(self):
+    """Pop a test from the collection.
+
+    Waits until a test is available or all tests have been handled.
+
+    Returns:
+      A test or None if all tests have been handled.
+    """
+    while True:
+      # Wait for a test to be available or all tests to have been handled.
+      self._item_available_or_all_done.wait()
+      with self._lock:
+        # Check which of the two conditions triggered the signal.
+        if self._tests_in_progress == 0:
+          return None
+        try:
+          return self._tests.pop(0)
+        except IndexError:
+          # Another thread beat us to the available test, wait again.
+          self._item_available_or_all_done.clear()
+
+  def add(self, test):
+    """Add a test to the collection.
+
+    Args:
+      test: A test to add.
+    """
+    with self._lock:
+      self._tests.append(test)
+      self._item_available_or_all_done.set()
+      self._tests_in_progress += 1
+
+  def test_completed(self):
+    """Indicate that a test has been fully handled."""
+    with self._lock:
+      self._tests_in_progress -= 1
+      if self._tests_in_progress == 0:
+        # All tests have been handled, signal all waiting threads.
+        self._item_available_or_all_done.set()
+
+  def __iter__(self):
+    """Iterate through tests in the collection until all have been handled."""
+    while True:
+      r = self._pop()
+      if r is None:
+        break
+      yield r
+
+  def __len__(self):
+    """Return the number of tests currently in the collection."""
+    return len(self._tests)
+
+  def test_names(self):
+    """Return a list of the names of the tests currently in the collection."""
+    with self._lock:
+      return list(t.test for t in self._tests)
diff --git a/src/build/android/pylib/base/test_exception.py b/src/build/android/pylib/base/test_exception.py
new file mode 100644
index 0000000..c98d2cb
--- /dev/null
+++ b/src/build/android/pylib/base/test_exception.py
@@ -0,0 +1,8 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+class TestException(Exception):
+  """Base class for exceptions thrown by the test runner."""
+  pass
diff --git a/src/build/android/pylib/base/test_instance.py b/src/build/android/pylib/base/test_instance.py
new file mode 100644
index 0000000..7b1099c
--- /dev/null
+++ b/src/build/android/pylib/base/test_instance.py
@@ -0,0 +1,40 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+class TestInstance(object):
+  """A type of test.
+
+  This is expected to handle all logic that is test-type specific but
+  independent of the environment or device.
+
+  Examples include:
+    - gtests
+    - instrumentation tests
+  """
+
+  def __init__(self):
+    pass
+
+  def TestType(self):
+    raise NotImplementedError
+
+  # pylint: disable=no-self-use
+  def GetPreferredAbis(self):
+    return None
+
+  # pylint: enable=no-self-use
+
+  def SetUp(self):
+    raise NotImplementedError
+
+  def TearDown(self):
+    raise NotImplementedError
+
+  def __enter__(self):
+    self.SetUp()
+    return self
+
+  def __exit__(self, _exc_type, _exc_val, _exc_tb):
+    self.TearDown()
diff --git a/src/build/android/pylib/base/test_instance_factory.py b/src/build/android/pylib/base/test_instance_factory.py
new file mode 100644
index 0000000..f47242a
--- /dev/null
+++ b/src/build/android/pylib/base/test_instance_factory.py
@@ -0,0 +1,26 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+from pylib.gtest import gtest_test_instance
+from pylib.instrumentation import instrumentation_test_instance
+from pylib.junit import junit_test_instance
+from pylib.monkey import monkey_test_instance
+from pylib.utils import device_dependencies
+
+
+def CreateTestInstance(args, error_func):
+
+  if args.command == 'gtest':
+    return gtest_test_instance.GtestTestInstance(
+        args, device_dependencies.GetDataDependencies, error_func)
+  elif args.command == 'instrumentation':
+    return instrumentation_test_instance.InstrumentationTestInstance(
+        args, device_dependencies.GetDataDependencies, error_func)
+  elif args.command == 'junit':
+    return junit_test_instance.JunitTestInstance(args, error_func)
+  elif args.command == 'monkey':
+    return monkey_test_instance.MonkeyTestInstance(args, error_func)
+
+  error_func('Unable to create %s test instance.' % args.command)
diff --git a/src/build/android/pylib/base/test_run.py b/src/build/android/pylib/base/test_run.py
new file mode 100644
index 0000000..fc72d3a
--- /dev/null
+++ b/src/build/android/pylib/base/test_run.py
@@ -0,0 +1,50 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+
+class TestRun(object):
+  """An execution of a particular test on a particular device.
+
+  This is expected to handle all logic that is specific to the combination of
+  environment and test type.
+
+  Examples include:
+    - local gtests
+    - local instrumentation tests
+  """
+
+  def __init__(self, env, test_instance):
+    self._env = env
+    self._test_instance = test_instance
+
+    # Some subclasses have different teardown behavior on receiving SIGTERM.
+    self._received_sigterm = False
+
+  def TestPackage(self):
+    raise NotImplementedError
+
+  def SetUp(self):
+    raise NotImplementedError
+
+  def RunTests(self, results):
+    """Runs Tests and populates |results|.
+
+    Args:
+      results: An array that should be populated with
+               |base_test_result.TestRunResults| objects.
+    """
+    raise NotImplementedError
+
+  def TearDown(self):
+    raise NotImplementedError
+
+  def __enter__(self):
+    self.SetUp()
+    return self
+
+  def __exit__(self, exc_type, exc_val, exc_tb):
+    self.TearDown()
+
+  def ReceivedSigterm(self):
+    self._received_sigterm = True
diff --git a/src/build/android/pylib/base/test_run_factory.py b/src/build/android/pylib/base/test_run_factory.py
new file mode 100644
index 0000000..35d5494
--- /dev/null
+++ b/src/build/android/pylib/base/test_run_factory.py
@@ -0,0 +1,36 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import absolute_import
+from pylib.gtest import gtest_test_instance
+from pylib.instrumentation import instrumentation_test_instance
+from pylib.junit import junit_test_instance
+from pylib.monkey import monkey_test_instance
+from pylib.local.device import local_device_environment
+from pylib.local.device import local_device_gtest_run
+from pylib.local.device import local_device_instrumentation_test_run
+from pylib.local.device import local_device_monkey_test_run
+from pylib.local.machine import local_machine_environment
+from pylib.local.machine import local_machine_junit_test_run
+
+
+def CreateTestRun(env, test_instance, error_func):
+  if isinstance(env, local_device_environment.LocalDeviceEnvironment):
+    if isinstance(test_instance, gtest_test_instance.GtestTestInstance):
+      return local_device_gtest_run.LocalDeviceGtestRun(env, test_instance)
+    if isinstance(test_instance,
+                  instrumentation_test_instance.InstrumentationTestInstance):
+      return (local_device_instrumentation_test_run
+              .LocalDeviceInstrumentationTestRun(env, test_instance))
+    if isinstance(test_instance, monkey_test_instance.MonkeyTestInstance):
+      return (local_device_monkey_test_run
+              .LocalDeviceMonkeyTestRun(env, test_instance))
+
+  if isinstance(env, local_machine_environment.LocalMachineEnvironment):
+    if isinstance(test_instance, junit_test_instance.JunitTestInstance):
+      return (local_machine_junit_test_run
+              .LocalMachineJunitTestRun(env, test_instance))
+
+  error_func('Unable to create test run for %s tests in %s environment'
+             % (str(test_instance), str(env)))
diff --git a/src/build/android/pylib/base/test_server.py b/src/build/android/pylib/base/test_server.py
new file mode 100644
index 0000000..763e121
--- /dev/null
+++ b/src/build/android/pylib/base/test_server.py
@@ -0,0 +1,18 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+class TestServer(object):
+  """Base class for any server that needs to be set up for the tests."""
+
+  def __init__(self, *args, **kwargs):
+    pass
+
+  def SetUp(self):
+    raise NotImplementedError
+
+  def Reset(self):
+    raise NotImplementedError
+
+  def TearDown(self):
+    raise NotImplementedError