Merge "Move tf python lib to platform_testing"
diff --git a/libraries/tradefed-python-lib/Android.bp b/libraries/tradefed-python-lib/Android.bp
new file mode 100644
index 0000000..13a3ff4
--- /dev/null
+++ b/libraries/tradefed-python-lib/Android.bp
@@ -0,0 +1,30 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Base library that should be extended/included to run as part of Tradefed
+python_library_host {
+  name: "tradefed_python_lib",
+  srcs: [
+    "tradefed_py/*.py",
+  ],
+  version: {
+    py2: {
+      enabled: true,
+    },
+    py3: {
+      enabled: false,
+    },
+  }
+}
+
diff --git a/libraries/tradefed-python-lib/OWNERS b/libraries/tradefed-python-lib/OWNERS
new file mode 100644
index 0000000..23f5f25
--- /dev/null
+++ b/libraries/tradefed-python-lib/OWNERS
@@ -0,0 +1,5 @@
+# Root Owners of the Tradefed python repo for code reviews
+dshi@google.com
+frankfeng@google.com
+guangzhu@google.com
+jdesprez@google.com
diff --git a/libraries/tradefed-python-lib/__init__.py b/libraries/tradefed-python-lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/libraries/tradefed-python-lib/__init__.py
diff --git a/libraries/tradefed-python-lib/helloWorld/Android.bp b/libraries/tradefed-python-lib/helloWorld/Android.bp
new file mode 100644
index 0000000..529331c
--- /dev/null
+++ b/libraries/tradefed-python-lib/helloWorld/Android.bp
@@ -0,0 +1,36 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Example hello world test that can run in Tradefed
+python_binary_host {
+  name: "tradefed_hello_world",
+  main: "test_hello_world.py",
+  srcs: [
+    "test_hello_world.py",
+  ],
+  libs: [
+    "tradefed_python_lib",
+  ],
+  test_suites: ["null-suite"],
+  version: {
+    py2: {
+      enabled: true,
+      embedded_launcher: true,
+    },
+    py3: {
+      enabled: false,
+    },
+  }
+}
+
diff --git a/libraries/tradefed-python-lib/helloWorld/AndroidTest.xml b/libraries/tradefed-python-lib/helloWorld/AndroidTest.xml
new file mode 100644
index 0000000..bce1f0a
--- /dev/null
+++ b/libraries/tradefed-python-lib/helloWorld/AndroidTest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-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.
+-->
+<configuration description="Config for python tradefed hello world">
+    <option name="test-suite-tag" value="python-tradefed" />
+    <test class="com.android.tradefed.testtype.python.PythonBinaryHostTest" >
+        <option name="par-file-name" value="tradefed_hello_world" />
+    </test>
+</configuration>
diff --git a/libraries/tradefed-python-lib/helloWorld/test_hello_world.py b/libraries/tradefed-python-lib/helloWorld/test_hello_world.py
new file mode 100644
index 0000000..4b5ad83
--- /dev/null
+++ b/libraries/tradefed-python-lib/helloWorld/test_hello_world.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+#
+# 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.
+#
+
+import unittest
+from tradefed_py import base_test
+from tradefed_py import tf_main
+
+class HelloWorldTest(base_test._TradefedTestClass):
+    """An example showing a possible implementation of python test"""
+
+    def test_hello(self):
+        self.assertEqual('hello'.upper(), 'HELLO')
+
+    def test_world_failed(self):
+        self.assertEqual('world'.upper(), 'WORLD2')
+
+    @unittest.skip('demonstrating skipping')
+    def test_skipped(self):
+        self.fail('should have been skipped')
+
+    @unittest.expectedFailure
+    def test_expectation(self):
+        self.fail('failed')
+
+    @unittest.expectedFailure
+    def test_failedExpectation(self):
+        pass
+
+    def test_device(self):
+        """If a serial was provided this test will check that we can query the
+        device. It will throw if the serial is invalid.
+        """
+        if self.serial is not None:
+            res = self.android_device.executeShellCommand('id')
+            self.assertTrue('uid' in res)
+
+if __name__ == '__main__':
+    tf_main.main()
diff --git a/libraries/tradefed-python-lib/tradefed_py/__init__.py b/libraries/tradefed-python-lib/tradefed_py/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/libraries/tradefed-python-lib/tradefed_py/__init__.py
diff --git a/libraries/tradefed-python-lib/tradefed_py/adb_handler.py b/libraries/tradefed-python-lib/tradefed_py/adb_handler.py
new file mode 100644
index 0000000..0464dba
--- /dev/null
+++ b/libraries/tradefed-python-lib/tradefed_py/adb_handler.py
@@ -0,0 +1,57 @@
+#
+# 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.
+#
+
+import subprocess
+
+class AdbHandler(object):
+    """Adb wrapper to execute shell and adb command to the device."""
+
+    def __init__(self, serial=None):
+        self.serial = serial
+        self.adb_cmd = 'adb -s {}'.format(serial)
+
+    def exec_adb_command(self, cmd):
+        """Method to execute an adb command against the device."""
+        cmd = "{} {}".format(self.adb_cmd, cmd)
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+        (out, err) = proc.communicate()
+        ret = proc.returncode
+        if ret == 0:
+            return out
+        raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
+
+    def exec_shell_command(self, cmd):
+        """Method to execute a shell command against the device."""
+        cmd = '{} shell {}'.format(self.adb_cmd, cmd)
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+        (out, err) = proc.communicate()
+        ret = proc.returncode
+        if ret == 0:
+            return out
+        raise AdbError(cmd=cmd, stdout=out, stderr=err, ret_code=ret)
+
+class AdbError(Exception):
+    """Raised when there is an error in adb operations."""
+
+    def __init__(self, cmd, stdout, stderr, ret_code):
+        self.cmd = cmd
+        self.stdout = stdout
+        self.stderr = stderr
+        self.ret_code = ret_code
+
+    def __str__(self):
+        return ('Error executing adb cmd "%s". ret: %d, stdout: %s, stderr: %s'
+                ) % (self.cmd, self.ret_code, self.stdout, self.stderr)
diff --git a/libraries/tradefed-python-lib/tradefed_py/android_device.py b/libraries/tradefed-python-lib/tradefed_py/android_device.py
new file mode 100644
index 0000000..568054f
--- /dev/null
+++ b/libraries/tradefed-python-lib/tradefed_py/android_device.py
@@ -0,0 +1,61 @@
+#
+# 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.
+#
+
+import adb_handler
+
+class AndroidTestDevice(object):
+    """Class representing an android device.
+
+    Each instance represents a different device connected to adb.
+    """
+
+    def __init__(self, serial=None, stream=None):
+        # TODO: Implement and flesh out the device interface
+        self.serial = serial
+        self._logging = stream
+        self.adb = adb_handler.AdbHandler(serial)
+
+    def executeShellCommand(self, cmd):
+        """Convenience method to call the adb wrapper to execute a shell command.
+
+        Args:
+            cmd: The command to be executed in 'adb shell'
+
+        Returns:
+            The stdout of the command if succeed. Or raise AdbError if failed.
+        """
+        return self.adb.exec_shell_command(cmd)
+
+    def getProp(self, name):
+        if not name:
+            raise DeviceCommandError('getProp', 'Name of property cannot be None')
+        out = self.executeShellCommand('getprop %s' % name)
+        return out.strip()
+
+    def _printHostLog(self, message):
+        self._logging.write('%s \n' % message)
+
+class DeviceCommandError(Exception):
+    """ Exception raised when an error is encountered while running a command.
+    """
+
+    def __init__(self, cmd, message):
+        self.cmd = cmd
+        self.message = message
+
+    def __str__(self):
+        return ('Error executing device cmd "%s". message: "%s"'
+                ) % (self.cmd, self.message)
diff --git a/libraries/tradefed-python-lib/tradefed_py/base_test.py b/libraries/tradefed-python-lib/tradefed_py/base_test.py
new file mode 100644
index 0000000..4fd916e
--- /dev/null
+++ b/libraries/tradefed-python-lib/tradefed_py/base_test.py
@@ -0,0 +1,53 @@
+#
+# 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.
+#
+
+import android_device
+import json
+import unittest
+
+_DATA_NAME = 'dataName'
+_DATA_TYPE = 'dataType'
+_DATA_FILE = 'dataFile'
+
+class _TradefedTestClass(unittest.TestCase):
+    """ A base test class to extends to receive a device object for testing in python
+
+        All tests should extends this class to be properly supported by Tradefed.
+    """
+
+    def setUpDevice(self, serial, stream, options):
+        """ Setter method that will allow the test to receive the device object
+
+        Args:
+            serial: The serial of the device allocated for the test.
+            stream: The output stream.
+            options: Additional options given to the tests that can be used.
+        """
+        self.serial = serial
+        self.stream = stream
+        self.extra_options = options
+        self.android_device = android_device.AndroidTestDevice(serial, stream)
+
+    def logFileToTradefed(self, name, filePath, fileType):
+        """ Callback to log a file that will be picked up by Tradefed.
+
+        Args:
+            name: The name under which log the particular data.
+            filePath: Absolute file path of the file to be logged.
+            fileType: The type of the file. (TEXT, PNG, etc.)
+        """
+        resp = {_DATA_NAME: name, _DATA_TYPE: fileType, _DATA_FILE: filePath}
+        self.stream.write('TEST_LOG %s\n' % json.dumps(resp))
diff --git a/libraries/tradefed-python-lib/tradefed_py/tf_main.py b/libraries/tradefed-python-lib/tradefed_py/tf_main.py
new file mode 100644
index 0000000..43a6aa5
--- /dev/null
+++ b/libraries/tradefed-python-lib/tradefed_py/tf_main.py
@@ -0,0 +1,97 @@
+#
+# 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.
+#
+
+import getopt
+import os
+import sys
+import tf_runner
+from unittest import loader
+import unittest
+
+class TradefedProgram(unittest.TestProgram):
+    """ Main Runner Class that should be used to run the tests. This runner ensure that the
+    reporting is compatible with Tradefed.
+    """
+
+    def __init__(self, module='__main__', defaultTest=None,
+                 argv=None, testRunner=None,
+                 testLoader=loader.defaultTestLoader, exit=True,
+                 verbosity=1, failfast=None, catchbreak=None, buffer=None, serial=None):
+      self.serial = None
+      self.extra_options = []
+      super(TradefedProgram, self).__init__()
+
+    def parseArgs(self, argv):
+        if len(argv) > 1 and argv[1].lower() == 'discover':
+            self._do_discovery(argv[2:])
+            return
+
+        long_opts = ['help', 'verbose', 'quiet', 'failfast', 'catch', 'buffer',
+                     'serial=', 'extra_options=']
+        try:
+            options, args = getopt.getopt(argv[1:], 'hHvqfcbs:e:', long_opts)
+            for opt, value in options:
+                if opt in ('-h','-H','--help'):
+                    self.usageExit()
+                if opt in ('-q','--quiet'):
+                    self.verbosity = 0
+                if opt in ('-v','--verbose'):
+                    self.verbosity = 2
+                if opt in ('-f','--failfast'):
+                    if self.failfast is None:
+                        self.failfast = True
+                    # Should this raise an exception if -f is not valid?
+                if opt in ('-c','--catch'):
+                    if self.catchbreak is None and installHandler is not None:
+                        self.catchbreak = True
+                    # Should this raise an exception if -c is not valid?
+                if opt in ('-b','--buffer'):
+                    if self.buffer is None:
+                        self.buffer = True
+                    # Should this raise an exception if -b is not valid?
+                if opt in ('-s', '--serial'):
+                    if self.serial is None:
+                        self.serial = value
+                if opt in ('-e', '--extra_options'):
+                    self.extra_options.append(value)
+            if len(args) == 0 and self.defaultTest is None:
+                # createTests will load tests from self.module
+                self.testNames = None
+            elif len(args) > 0:
+                self.testNames = args
+                if __name__ == '__main__':
+                    # to support python -m unittest ...
+                    self.module = None
+            else:
+                self.testNames = (self.defaultTest,)
+            self.createTests()
+        except getopt.error, msg:
+            self.usageExit(msg)
+
+    def runTests(self):
+        if self.testRunner is None:
+            self.testRunner = tf_runner.TfTextTestRunner(verbosity=self.verbosity,
+                                                         failfast=self.failfast,
+                                                         buffer=self.buffer,
+                                                         resultclass=tf_runner.TextTestResult,
+                                                         serial=self.serial,
+                                                         extra_options=self.extra_options)
+        super(TradefedProgram, self).runTests()
+
+main = TradefedProgram
+
+def main_run():
+    TradefedProgram(module=None)
diff --git a/libraries/tradefed-python-lib/tradefed_py/tf_runner.py b/libraries/tradefed-python-lib/tradefed_py/tf_runner.py
new file mode 100644
index 0000000..d571082
--- /dev/null
+++ b/libraries/tradefed-python-lib/tradefed_py/tf_runner.py
@@ -0,0 +1,227 @@
+#
+# 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.
+#
+
+import sys
+import json
+import time
+import traceback
+import unittest
+from unittest.util import strclass
+from unittest.signals import registerResult
+
+# Tags that Tradefed can understand in SubprocessResultParser to get results
+_CLASSNAME_TAG = 'className'
+_METHOD_NAME_TAG = 'testName'
+_START_TIME_TAG = 'start_time'
+_END_TIME_TAG = 'end_time'
+_TRACE_TAG = 'trace'
+_TEST_COUNT_TAG = 'testCount'
+_REASON_TAG = 'reason'
+_TIME_TAG = 'time'
+
+class TextTestResult(unittest.TextTestResult):
+    """ Class for callbacks based on test state"""
+
+    def _getClassName(self, test):
+        return strclass(test.__class__)
+
+    def _getMethodName(self, test):
+        return test._testMethodName
+
+    def startTestRun(self, count):
+        """ Callback that marks a test run has started.
+
+        Args:
+            count: The number of expected tests.
+        """
+        resp = {_TEST_COUNT_TAG: count, 'runName': 'python-tradefed'}
+        self.stream.write('TEST_RUN_STARTED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).startTestRun()
+
+    def startTest(self, test):
+        """ Callback that marks a test has started to run.
+
+        Args:
+            test: The test that started.
+        """
+        resp = {_START_TIME_TAG: time.time(), _CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test)}
+        self.stream.write('TEST_STARTED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).startTest(test)
+
+    def addSuccess(self, test):
+        """ Callback that marks a test has finished and passed
+
+        Args:
+            test: The test that passed.
+        """
+        resp = {_END_TIME_TAG: time.time(), _CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test)}
+        self.stream.write('TEST_ENDED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).addSuccess(test)
+
+    def addFailure(self, test, err):
+        """ Callback that marks a test has failed
+
+        Args:
+            test: The test that failed.
+            err: the error generated that should be reported.
+        """
+        resp = {_CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test), _TRACE_TAG: '\n'.join(traceback.format_exception(*err))}
+        self.stream.write('TEST_FAILED %s\n' % json.dumps(resp))
+        resp = {_END_TIME_TAG: time.time(), _CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test)}
+        self.stream.write('TEST_ENDED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).addFailure(test, err)
+
+    def addSkip(self, test, reason):
+        """ Callback that marks a test was being skipped
+
+        Args:
+            test: The test being skipped.
+            reason: the message generated that should be reported.
+        """
+        resp = {_CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test)}
+        self.stream.write('TEST_IGNORED %s\n' % json.dumps(resp))
+        resp = {_END_TIME_TAG: time.time(), _CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test)}
+        self.stream.write('TEST_ENDED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).addSkip(test, reason)
+
+    def addExpectedFailure(self, test, err):
+        """ Callback that marks a test was expected to fail and failed.
+
+        Args:
+            test: The test responsible for the error.
+            err: the error generated that should be reported.
+        """
+        resp = {_CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test), _TRACE_TAG: '\n'.join(traceback.format_exception(*err))}
+        self.stream.write('TEST_ASSUMPTION_FAILURE %s\n' % json.dumps(resp))
+        resp = {_END_TIME_TAG: time.time(), _CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test)}
+        self.stream.write('TEST_ENDED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).addExpectedFailure(test, err)
+
+    def addUnexpectedSuccess(self, test):
+        """ Callback that marks a test was expected to fail but passed.
+
+        Args:
+            test: The test responsible for the unexpected success.
+        """
+        resp = {_CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test), _TRACE_TAG: 'Unexpected success'}
+        self.stream.write('TEST_ASSUMPTION_FAILURE %s\n' % json.dumps(resp))
+        resp = {_END_TIME_TAG: time.time(), _CLASSNAME_TAG: self._getClassName(test), _METHOD_NAME_TAG: self._getMethodName(test)}
+        self.stream.write('TEST_ENDED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).addUnexpectedSuccess(test)
+
+    def addError(self, test, err):
+        """ Callback that marks a run as failed because of an error.
+
+        Args:
+            test: The test responsible for the error.
+            err: the error generated that should be reported.
+        """
+        resp = {_REASON_TAG: '\n'.join(traceback.format_exception(*err))}
+        self.stream.write('TEST_RUN_FAILED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).addError(test, err)
+
+    def stopTestRun(self, elapsedTime):
+        """ Callback that marks the end of a test run
+
+        Args:
+            elapsedTime: The elapsed time of the run.
+        """
+        resp = {_TIME_TAG: elapsedTime}
+        self.stream.write('TEST_RUN_ENDED %s\n' % json.dumps(resp))
+        super(TextTestResult, self).stopTestRun()
+
+class TfTextTestRunner(unittest.TextTestRunner):
+    """ Class runner that ensure the callbacks order"""
+
+    def __init__(self, stream=sys.stderr, descriptions=True, verbosity=1,
+                 failfast=False, buffer=False, resultclass=None, serial=None, extra_options=None):
+        self.serial = serial
+        self.extra_options = extra_options
+        unittest.TextTestRunner.__init__(self, stream, descriptions, verbosity, failfast, buffer, resultclass)
+
+    def _injectDevice(self, testSuites):
+        """ Method to inject options to the base Python Tradefed class
+
+        Args:
+            testSuites: the current test holder.
+        """
+        if self.serial is not None:
+            for testSuite in testSuites:
+                # each test in the test suite
+                for test in testSuite._tests:
+                    try:
+                        test.setUpDevice(self.serial, self.stream, self.extra_options)
+                    except AttributeError:
+                        self.stream.writeln('Test %s does not implement _TradefedTestClass.' % test)
+
+    def run(self, test):
+        """ Run the given test case or test suite. Copied from unittest to replace the startTestRun
+        callback"""
+        result = self._makeResult()
+        result.failfast = self.failfast
+        result.buffer = self.buffer
+        registerResult(result)
+        startTime = time.time()
+        startTestRun = getattr(result, 'startTestRun', None)
+        if startTestRun is not None:
+            startTestRun(test.countTestCases())
+        try:
+            self._injectDevice(test)
+            test(result)
+        finally:
+            stopTestRun = getattr(result, 'stopTestRun', None)
+            if stopTestRun is not None:
+                stopTestRun(time.time() - startTime)
+            else:
+                result.printErrors()
+        stopTime = time.time()
+        timeTaken = stopTime - startTime
+        if hasattr(result, 'separator2'):
+            self.stream.writeln(result.separator2)
+        run = result.testsRun
+        self.stream.writeln('Ran %d test%s in %.3fs' %
+                            (run, run != 1 and 's' or '', timeTaken))
+        self.stream.writeln()
+
+        expectedFails = unexpectedSuccesses = skipped = 0
+        try:
+            results = map(len, (result.expectedFailures,
+                                result.unexpectedSuccesses,
+                                result.skipped))
+            expectedFails, unexpectedSuccesses, skipped = results
+        except AttributeError:
+            pass
+        infos = []
+        if not result.wasSuccessful():
+            self.stream.write('FAILED')
+            failed, errored = map(len, (result.failures, result.errors))
+            if failed:
+                infos.append('failures=%d' % failed)
+            if errored:
+                infos.append('errors=%d' % errored)
+        else:
+            self.stream.write('OK')
+        if skipped:
+            infos.append('skipped=%d' % skipped)
+        if expectedFails:
+            infos.append('expected failures=%d' % expectedFails)
+        if unexpectedSuccesses:
+            infos.append('unexpected successes=%d' % unexpectedSuccesses)
+        if infos:
+            self.stream.writeln(' (%s)' % (', '.join(infos),))
+        else:
+            self.stream.write('\n')
+        return result