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