aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/python/grpcio/tests
diff options
context:
space:
mode:
Diffstat (limited to 'src/python/grpcio/tests')
-rw-r--r--src/python/grpcio/tests/__init__.py34
-rw-r--r--src/python/grpcio/tests/_loader.py119
-rw-r--r--src/python/grpcio/tests/_result.py451
-rw-r--r--src/python/grpcio/tests/_runner.py224
-rw-r--r--src/python/grpcio/tests/interop/__init__.py30
-rw-r--r--src/python/grpcio/tests/interop/_insecure_interop_test.py58
-rw-r--r--src/python/grpcio/tests/interop/_interop_test_case.py64
-rw-r--r--src/python/grpcio/tests/interop/_secure_interop_test.py67
-rw-r--r--src/python/grpcio/tests/interop/client.py124
-rw-r--r--src/python/grpcio/tests/interop/credentials/README1
-rwxr-xr-xsrc/python/grpcio/tests/interop/credentials/ca.pem15
-rwxr-xr-xsrc/python/grpcio/tests/interop/credentials/server1.key16
-rwxr-xr-xsrc/python/grpcio/tests/interop/credentials/server1.pem16
-rw-r--r--src/python/grpcio/tests/interop/empty.proto43
-rw-r--r--src/python/grpcio/tests/interop/messages.proto167
-rw-r--r--src/python/grpcio/tests/interop/methods.py341
-rw-r--r--src/python/grpcio/tests/interop/resources.py61
-rw-r--r--src/python/grpcio/tests/interop/server.py75
-rw-r--r--src/python/grpcio/tests/interop/test.proto86
-rw-r--r--src/python/grpcio/tests/protoc_plugin/__init__.py30
-rw-r--r--src/python/grpcio/tests/protoc_plugin/beta_python_plugin_test.py524
-rw-r--r--src/python/grpcio/tests/protoc_plugin/protoc_plugin_test.proto139
-rw-r--r--src/python/grpcio/tests/unit/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/_adapter/.gitignore5
-rw-r--r--src/python/grpcio/tests/unit/_adapter/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/_adapter/_intermediary_low_test.py426
-rw-r--r--src/python/grpcio/tests/unit/_adapter/_low_test.py319
-rw-r--r--src/python/grpcio/tests/unit/_adapter/_proto_scenarios.py261
-rw-r--r--src/python/grpcio/tests/unit/_core_over_links_base_interface_test.py155
-rw-r--r--src/python/grpcio/tests/unit/_crust_over_core_over_links_face_interface_test.py161
-rw-r--r--src/python/grpcio/tests/unit/_cython/.gitignore7
-rw-r--r--src/python/grpcio/tests/unit/_cython/__init__.py28
-rw-r--r--src/python/grpcio/tests/unit/_cython/cygrpc_test.py452
-rw-r--r--src/python/grpcio/tests/unit/_cython/test_utilities.py46
-rw-r--r--src/python/grpcio/tests/unit/_junkdrawer/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/_junkdrawer/math_pb2.py266
-rw-r--r--src/python/grpcio/tests/unit/_junkdrawer/stock_pb2.py152
-rw-r--r--src/python/grpcio/tests/unit/_links/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/_links/_lonely_invocation_link_test.py88
-rw-r--r--src/python/grpcio/tests/unit/_links/_proto_scenarios.py261
-rw-r--r--src/python/grpcio/tests/unit/_links/_transmission_test.py239
-rw-r--r--src/python/grpcio/tests/unit/beta/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/beta/_beta_features_test.py343
-rw-r--r--src/python/grpcio/tests/unit/beta/_connectivity_channel_test.py191
-rw-r--r--src/python/grpcio/tests/unit/beta/_face_interface_test.py138
-rw-r--r--src/python/grpcio/tests/unit/beta/_not_found_test.py75
-rw-r--r--src/python/grpcio/tests/unit/beta/_utilities_test.py123
-rw-r--r--src/python/grpcio/tests/unit/beta/test_utilities.py56
-rw-r--r--src/python/grpcio/tests/unit/credentials/README1
-rwxr-xr-xsrc/python/grpcio/tests/unit/credentials/ca.pem15
-rwxr-xr-xsrc/python/grpcio/tests/unit/credentials/server1.key16
-rwxr-xr-xsrc/python/grpcio/tests/unit/credentials/server1.pem16
-rw-r--r--src/python/grpcio/tests/unit/framework/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/_crust_over_core_face_interface_test.py111
-rw-r--r--src/python/grpcio/tests/unit/framework/common/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/common/test_constants.py53
-rw-r--r--src/python/grpcio/tests/unit/framework/common/test_control.py95
-rw-r--r--src/python/grpcio/tests/unit/framework/common/test_coverage.py116
-rw-r--r--src/python/grpcio/tests/unit/framework/core/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/core/_base_interface_test.py96
-rw-r--r--src/python/grpcio/tests/unit/framework/face/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/base_util.py102
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/blocking_invocation_inline_service_test_case.py222
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/callback.py94
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/control.py87
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/coverage.py123
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/digest.py450
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/event_invocation_synchronous_event_service_test_case.py376
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/future_invocation_asynchronous_event_service_test_case.py379
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/interfaces.py117
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/service.py337
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/stock_service.py374
-rw-r--r--src/python/grpcio/tests/unit/framework/face/testing/test_case.py80
-rw-r--r--src/python/grpcio/tests/unit/framework/foundation/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/foundation/_later_test.py151
-rw-r--r--src/python/grpcio/tests/unit/framework/foundation/_logging_pool_test.py64
-rw-r--r--src/python/grpcio/tests/unit/framework/foundation/stream_testing.py73
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/base/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/base/_control.py568
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/base/_sequence.py171
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/base/_state.py55
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/base/test_cases.py277
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/base/test_interfaces.py186
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_3069_test_constant.py37
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_blocking_invocation_inline_service.py252
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_digest.py444
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_event_invocation_synchronous_event_service.py381
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_future_invocation_asynchronous_event_service.py435
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_invocation.py213
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_receiver.py95
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_service.py332
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/_stock_service.py396
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/test_cases.py69
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/face/test_interfaces.py229
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/links/__init__.py30
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/links/test_cases.py326
-rw-r--r--src/python/grpcio/tests/unit/framework/interfaces/links/test_utilities.py167
-rw-r--r--src/python/grpcio/tests/unit/resources.py52
-rw-r--r--src/python/grpcio/tests/unit/test_common.py80
102 files changed, 15240 insertions, 0 deletions
diff --git a/src/python/grpcio/tests/__init__.py b/src/python/grpcio/tests/__init__.py
new file mode 100644
index 0000000000..b76b3985a1
--- /dev/null
+++ b/src/python/grpcio/tests/__init__.py
@@ -0,0 +1,34 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from tests import _loader
+from tests import _runner
+
+Loader = _loader.Loader
+Runner = _runner.Runner
diff --git a/src/python/grpcio/tests/_loader.py b/src/python/grpcio/tests/_loader.py
new file mode 100644
index 0000000000..6992029b5e
--- /dev/null
+++ b/src/python/grpcio/tests/_loader.py
@@ -0,0 +1,119 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import importlib
+import pkgutil
+import re
+import unittest
+
+import coverage
+
+TEST_MODULE_REGEX = r'^.*_test$'
+
+
+class Loader(object):
+ """Test loader for setuptools test suite support.
+
+ Attributes:
+ suite (unittest.TestSuite): All tests collected by the loader.
+ loader (unittest.TestLoader): Standard Python unittest loader to be ran per
+ module discovered.
+ module_matcher (re.RegexObject): A regular expression object to match
+ against module names and determine whether or not the discovered module
+ contributes to the test suite.
+ """
+
+ def __init__(self):
+ self.suite = unittest.TestSuite()
+ self.loader = unittest.TestLoader()
+ self.module_matcher = re.compile(TEST_MODULE_REGEX)
+
+ def loadTestsFromNames(self, names, module=None):
+ """Function mirroring TestLoader::loadTestsFromNames, as expected by
+ setuptools.setup argument `test_loader`."""
+ # ensure that we capture decorators and definitions (else our coverage
+ # measure unnecessarily suffers)
+ coverage_context = coverage.Coverage(data_suffix=True)
+ coverage_context.start()
+ modules = [importlib.import_module(name) for name in names]
+ for module in modules:
+ self.visit_module(module)
+ for module in modules:
+ try:
+ package_paths = module.__path__
+ except:
+ continue
+ self.walk_packages(package_paths)
+ coverage_context.stop()
+ coverage_context.save()
+ return self.suite
+
+ def walk_packages(self, package_paths):
+ """Walks over the packages, dispatching `visit_module` calls.
+
+ Args:
+ package_paths (list): A list of paths over which to walk through modules
+ along.
+ """
+ for importer, module_name, is_package in (
+ pkgutil.iter_modules(package_paths)):
+ module = importer.find_module(module_name).load_module(module_name)
+ self.visit_module(module)
+ if is_package:
+ self.walk_packages(module.__path__)
+
+ def visit_module(self, module):
+ """Visits the module, adding discovered tests to the test suite.
+
+ Args:
+ module (module): Module to match against self.module_matcher; if matched
+ it has its tests loaded via self.loader into self.suite.
+ """
+ if self.module_matcher.match(module.__name__):
+ module_suite = self.loader.loadTestsFromModule(module)
+ self.suite.addTest(module_suite)
+
+
+def iterate_suite_cases(suite):
+ """Generator over all unittest.TestCases in a unittest.TestSuite.
+
+ Args:
+ suite (unittest.TestSuite): Suite to iterate over in the generator.
+
+ Returns:
+ generator: A generator over all unittest.TestCases in `suite`.
+ """
+ for item in suite:
+ if isinstance(item, unittest.TestSuite):
+ for child_item in iterate_suite_cases(item):
+ yield child_item
+ elif isinstance(item, unittest.TestCase):
+ yield item
+ else:
+ raise ValueError('unexpected suite item of type {}'.format(type(item)))
diff --git a/src/python/grpcio/tests/_result.py b/src/python/grpcio/tests/_result.py
new file mode 100644
index 0000000000..5a570f4279
--- /dev/null
+++ b/src/python/grpcio/tests/_result.py
@@ -0,0 +1,451 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import cStringIO as StringIO
+import collections
+import itertools
+import traceback
+import unittest
+from xml.etree import ElementTree
+
+import coverage
+
+from tests import _loader
+
+
+class CaseResult(collections.namedtuple('CaseResult', [
+ 'id', 'name', 'kind', 'stdout', 'stderr', 'skip_reason', 'traceback'])):
+ """A serializable result of a single test case.
+
+ Attributes:
+ id (object): Any serializable object used to denote the identity of this
+ test case.
+ name (str or None): A human-readable name of the test case.
+ kind (CaseResult.Kind): The kind of test result.
+ stdout (object or None): Output on stdout, or None if nothing was captured.
+ stderr (object or None): Output on stderr, or None if nothing was captured.
+ skip_reason (object or None): The reason the test was skipped. Must be
+ something if self.kind is CaseResult.Kind.SKIP, else None.
+ traceback (object or None): The traceback of the test. Must be something if
+ self.kind is CaseResult.Kind.{ERROR, FAILURE, EXPECTED_FAILURE}, else
+ None.
+ """
+
+ class Kind:
+ UNTESTED = 'untested'
+ RUNNING = 'running'
+ ERROR = 'error'
+ FAILURE = 'failure'
+ SUCCESS = 'success'
+ SKIP = 'skip'
+ EXPECTED_FAILURE = 'expected failure'
+ UNEXPECTED_SUCCESS = 'unexpected success'
+
+ def __new__(cls, id=None, name=None, kind=None, stdout=None, stderr=None,
+ skip_reason=None, traceback=None):
+ """Helper keyword constructor for the namedtuple.
+
+ See this class' attributes for information on the arguments."""
+ assert id is not None
+ assert name is None or isinstance(name, str)
+ if kind is CaseResult.Kind.UNTESTED:
+ pass
+ elif kind is CaseResult.Kind.RUNNING:
+ pass
+ elif kind is CaseResult.Kind.ERROR:
+ assert traceback is not None
+ elif kind is CaseResult.Kind.FAILURE:
+ assert traceback is not None
+ elif kind is CaseResult.Kind.SUCCESS:
+ pass
+ elif kind is CaseResult.Kind.SKIP:
+ assert skip_reason is not None
+ elif kind is CaseResult.Kind.EXPECTED_FAILURE:
+ assert traceback is not None
+ elif kind is CaseResult.Kind.UNEXPECTED_SUCCESS:
+ pass
+ else:
+ assert False
+ return super(cls, CaseResult).__new__(
+ cls, id, name, kind, stdout, stderr, skip_reason, traceback)
+
+ def updated(self, name=None, kind=None, stdout=None, stderr=None,
+ skip_reason=None, traceback=None):
+ """Get a new validated CaseResult with the fields updated.
+
+ See this class' attributes for information on the arguments."""
+ name = self.name if name is None else name
+ kind = self.kind if kind is None else kind
+ stdout = self.stdout if stdout is None else stdout
+ stderr = self.stderr if stderr is None else stderr
+ skip_reason = self.skip_reason if skip_reason is None else skip_reason
+ traceback = self.traceback if traceback is None else traceback
+ return CaseResult(id=self.id, name=name, kind=kind, stdout=stdout,
+ stderr=stderr, skip_reason=skip_reason,
+ traceback=traceback)
+
+
+class AugmentedResult(unittest.TestResult):
+ """unittest.Result that keeps track of additional information.
+
+ Uses CaseResult objects to store test-case results, providing additional
+ information beyond that of the standard Python unittest library, such as
+ standard output.
+
+ Attributes:
+ id_map (callable): A unary callable mapping unittest.TestCase objects to
+ unique identifiers.
+ cases (dict): A dictionary mapping from the identifiers returned by id_map
+ to CaseResult objects corresponding to those IDs.
+ """
+
+ def __init__(self, id_map):
+ """Initialize the object with an identifier mapping.
+
+ Arguments:
+ id_map (callable): Corresponds to the attribute `id_map`."""
+ super(AugmentedResult, self).__init__()
+ self.id_map = id_map
+ self.cases = None
+
+ def startTestRun(self):
+ """See unittest.TestResult.startTestRun."""
+ super(AugmentedResult, self).startTestRun()
+ self.cases = dict()
+
+ def stopTestRun(self):
+ """See unittest.TestResult.stopTestRun."""
+ super(AugmentedResult, self).stopTestRun()
+
+ def startTest(self, test):
+ """See unittest.TestResult.startTest."""
+ super(AugmentedResult, self).startTest(test)
+ case_id = self.id_map(test)
+ self.cases[case_id] = CaseResult(
+ id=case_id, name=test.id(), kind=CaseResult.Kind.RUNNING)
+
+ def addError(self, test, error):
+ """See unittest.TestResult.addError."""
+ super(AugmentedResult, self).addError(test, error)
+ case_id = self.id_map(test)
+ self.cases[case_id] = self.cases[case_id].updated(
+ kind=CaseResult.Kind.ERROR, traceback=error)
+
+ def addFailure(self, test, error):
+ """See unittest.TestResult.addFailure."""
+ super(AugmentedResult, self).addFailure(test, error)
+ case_id = self.id_map(test)
+ self.cases[case_id] = self.cases[case_id].updated(
+ kind=CaseResult.Kind.FAILURE, traceback=error)
+
+ def addSuccess(self, test):
+ """See unittest.TestResult.addSuccess."""
+ super(AugmentedResult, self).addSuccess(test)
+ case_id = self.id_map(test)
+ self.cases[case_id] = self.cases[case_id].updated(
+ kind=CaseResult.Kind.SUCCESS)
+
+ def addSkip(self, test, reason):
+ """See unittest.TestResult.addSkip."""
+ super(AugmentedResult, self).addSkip(test, reason)
+ case_id = self.id_map(test)
+ self.cases[case_id] = self.cases[case_id].updated(
+ kind=CaseResult.Kind.SKIP, skip_reason=reason)
+
+ def addExpectedFailure(self, test, error):
+ """See unittest.TestResult.addExpectedFailure."""
+ super(AugmentedResult, self).addExpectedFailure(test, error)
+ case_id = self.id_map(test)
+ self.cases[case_id] = self.cases[case_id].updated(
+ kind=CaseResult.Kind.EXPECTED_FAILURE, traceback=error)
+
+ def addUnexpectedSuccess(self, test):
+ """See unittest.TestResult.addUnexpectedSuccess."""
+ super(AugmentedResult, self).addUnexpectedSuccess(test)
+ case_id = self.id_map(test)
+ self.cases[case_id] = self.cases[case_id].updated(
+ kind=CaseResult.Kind.UNEXPECTED_SUCCESS)
+
+ def set_output(self, test, stdout, stderr):
+ """Set the output attributes for the CaseResult corresponding to a test.
+
+ Args:
+ test (unittest.TestCase): The TestCase to set the outputs of.
+ stdout (str): Output from stdout to assign to self.id_map(test).
+ stderr (str): Output from stderr to assign to self.id_map(test).
+ """
+ case_id = self.id_map(test)
+ self.cases[case_id] = self.cases[case_id].updated(
+ stdout=stdout, stderr=stderr)
+
+ def augmented_results(self, filter):
+ """Convenience method to retrieve filtered case results.
+
+ Args:
+ filter (callable): A unary predicate to filter over CaseResult objects.
+ """
+ return (self.cases[case_id] for case_id in self.cases
+ if filter(self.cases[case_id]))
+
+
+class CoverageResult(AugmentedResult):
+ """Extension to AugmentedResult adding coverage.py support per test.\
+
+ Attributes:
+ coverage_context (coverage.Coverage): coverage.py management object.
+ """
+
+ def __init__(self, id_map):
+ """See AugmentedResult.__init__."""
+ super(CoverageResult, self).__init__(id_map=id_map)
+ self.coverage_context = None
+
+ def startTest(self, test):
+ """See unittest.TestResult.startTest.
+
+ Additionally initializes and begins code coverage tracking."""
+ super(CoverageResult, self).startTest(test)
+ self.coverage_context = coverage.Coverage(data_suffix=True)
+ self.coverage_context.start()
+
+ def stopTest(self, test):
+ """See unittest.TestResult.stopTest.
+
+ Additionally stops and deinitializes code coverage tracking."""
+ super(CoverageResult, self).stopTest(test)
+ self.coverage_context.stop()
+ self.coverage_context.save()
+ self.coverage_context = None
+
+ def stopTestRun(self):
+ """See unittest.TestResult.stopTestRun."""
+ super(CoverageResult, self).stopTestRun()
+ # TODO(atash): Dig deeper into why the following line fails to properly
+ # combine coverage data from the Cython plugin.
+ #coverage.Coverage().combine()
+
+
+class _Colors:
+ """Namespaced constants for terminal color magic numbers."""
+ HEADER = '\033[95m'
+ INFO = '\033[94m'
+ OK = '\033[92m'
+ WARN = '\033[93m'
+ FAIL = '\033[91m'
+ BOLD = '\033[1m'
+ UNDERLINE = '\033[4m'
+ END = '\033[0m'
+
+
+class TerminalResult(CoverageResult):
+ """Extension to CoverageResult adding basic terminal reporting."""
+
+ def __init__(self, out, id_map):
+ """Initialize the result object.
+
+ Args:
+ out (file-like): Output file to which terminal-colored live results will
+ be written.
+ id_map (callable): See AugmentedResult.__init__.
+ """
+ super(TerminalResult, self).__init__(id_map=id_map)
+ self.out = out
+
+ def startTestRun(self):
+ """See unittest.TestResult.startTestRun."""
+ super(TerminalResult, self).startTestRun()
+ self.out.write(
+ _Colors.HEADER +
+ 'Testing gRPC Python...\n' +
+ _Colors.END)
+
+ def stopTestRun(self):
+ """See unittest.TestResult.stopTestRun."""
+ super(TerminalResult, self).stopTestRun()
+ self.out.write(summary(self))
+ self.out.flush()
+
+ def addError(self, test, error):
+ """See unittest.TestResult.addError."""
+ super(TerminalResult, self).addError(test, error)
+ self.out.write(
+ _Colors.FAIL +
+ 'ERROR {}\n'.format(test.id()) +
+ _Colors.END)
+ self.out.flush()
+
+ def addFailure(self, test, error):
+ """See unittest.TestResult.addFailure."""
+ super(TerminalResult, self).addFailure(test, error)
+ self.out.write(
+ _Colors.FAIL +
+ 'FAILURE {}\n'.format(test.id()) +
+ _Colors.END)
+ self.out.flush()
+
+ def addSuccess(self, test):
+ """See unittest.TestResult.addSuccess."""
+ super(TerminalResult, self).addSuccess(test)
+ self.out.write(
+ _Colors.OK +
+ 'SUCCESS {}\n'.format(test.id()) +
+ _Colors.END)
+ self.out.flush()
+
+ def addSkip(self, test, reason):
+ """See unittest.TestResult.addSkip."""
+ super(TerminalResult, self).addSkip(test, reason)
+ self.out.write(
+ _Colors.INFO +
+ 'SKIP {}\n'.format(test.id()) +
+ _Colors.END)
+ self.out.flush()
+
+ def addExpectedFailure(self, test, error):
+ """See unittest.TestResult.addExpectedFailure."""
+ super(TerminalResult, self).addExpectedFailure(test, error)
+ self.out.write(
+ _Colors.INFO +
+ 'FAILURE_OK {}\n'.format(test.id()) +
+ _Colors.END)
+ self.out.flush()
+
+ def addUnexpectedSuccess(self, test):
+ """See unittest.TestResult.addUnexpectedSuccess."""
+ super(TerminalResult, self).addUnexpectedSuccess(test)
+ self.out.write(
+ _Colors.INFO +
+ 'UNEXPECTED_OK {}\n'.format(test.id()) +
+ _Colors.END)
+ self.out.flush()
+
+def _traceback_string(type, value, trace):
+ """Generate a descriptive string of a Python exception traceback.
+
+ Args:
+ type (class): The type of the exception.
+ value (Exception): The value of the exception.
+ trace (traceback): Traceback of the exception.
+
+ Returns:
+ str: Formatted exception descriptive string.
+ """
+ buffer = StringIO.StringIO()
+ traceback.print_exception(type, value, trace, file=buffer)
+ return buffer.getvalue()
+
+def summary(result):
+ """A summary string of a result object.
+
+ Args:
+ result (AugmentedResult): The result object to get the summary of.
+
+ Returns:
+ str: The summary string.
+ """
+ assert isinstance(result, AugmentedResult)
+ untested = list(result.augmented_results(
+ lambda case_result: case_result.kind is CaseResult.Kind.UNTESTED))
+ running = list(result.augmented_results(
+ lambda case_result: case_result.kind is CaseResult.Kind.RUNNING))
+ failures = list(result.augmented_results(
+ lambda case_result: case_result.kind is CaseResult.Kind.FAILURE))
+ errors = list(result.augmented_results(
+ lambda case_result: case_result.kind is CaseResult.Kind.ERROR))
+ successes = list(result.augmented_results(
+ lambda case_result: case_result.kind is CaseResult.Kind.SUCCESS))
+ skips = list(result.augmented_results(
+ lambda case_result: case_result.kind is CaseResult.Kind.SKIP))
+ expected_failures = list(result.augmented_results(
+ lambda case_result: case_result.kind is CaseResult.Kind.EXPECTED_FAILURE))
+ unexpected_successes = list(result.augmented_results(
+ lambda case_result: case_result.kind is CaseResult.Kind.UNEXPECTED_SUCCESS))
+ running_names = [case.name for case in running]
+ finished_count = (len(failures) + len(errors) + len(successes) +
+ len(expected_failures) + len(unexpected_successes))
+ statistics = (
+ '{finished} tests finished:\n'
+ '\t{successful} successful\n'
+ '\t{unsuccessful} unsuccessful\n'
+ '\t{skipped} skipped\n'
+ '\t{expected_fail} expected failures\n'
+ '\t{unexpected_successful} unexpected successes\n'
+ 'Interrupted Tests:\n'
+ '\t{interrupted}\n'
+ .format(finished=finished_count,
+ successful=len(successes),
+ unsuccessful=(len(failures)+len(errors)),
+ skipped=len(skips),
+ expected_fail=len(expected_failures),
+ unexpected_successful=len(unexpected_successes),
+ interrupted=str(running_names)))
+ tracebacks = '\n\n'.join([
+ (_Colors.FAIL + '{test_name}' + _Colors.END + + '\n' +
+ _Colors.BOLD + 'traceback:' + _Colors.END + '\n' +
+ '{traceback}\n' +
+ _Colors.BOLD + 'stdout:' + _Colors.END + '\n' +
+ '{stdout}\n' +
+ _Colors.BOLD + 'stderr:' + _Colors.END + '\n' +
+ '{stderr}\n').format(
+ test_name=result.name,
+ traceback=_traceback_string(*result.traceback),
+ stdout=result.stdout, stderr=result.stderr)
+ for result in itertools.chain(failures, errors)
+ ])
+ notes = 'Unexpected successes: {}\n'.format([
+ result.name for result in unexpected_successes])
+ return statistics + '\nErrors/Failures: \n' + tracebacks + '\n' + notes
+
+
+def jenkins_junit_xml(result):
+ """An XML tree object that when written is recognizable by Jenkins.
+
+ Args:
+ result (AugmentedResult): The result object to get the junit xml output of.
+
+ Returns:
+ ElementTree.ElementTree: The XML tree.
+ """
+ assert isinstance(result, AugmentedResult)
+ root = ElementTree.Element('testsuites')
+ suite = ElementTree.SubElement(root, 'testsuite', {
+ 'name': 'Python gRPC tests',
+ })
+ for case in result.cases.values():
+ if case.kind is CaseResult.Kind.SUCCESS:
+ ElementTree.SubElement(suite, 'testcase', {
+ 'name': case.name,
+ })
+ elif case.kind in (CaseResult.Kind.ERROR, CaseResult.Kind.FAILURE):
+ case_xml = ElementTree.SubElement(suite, 'testcase', {
+ 'name': case.name,
+ })
+ error_xml = ElementTree.SubElement(case_xml, 'error', {})
+ error_xml.text = ''.format(case.stderr, case.traceback)
+ return ElementTree.ElementTree(element=root)
diff --git a/src/python/grpcio/tests/_runner.py b/src/python/grpcio/tests/_runner.py
new file mode 100644
index 0000000000..4f1ddb57fc
--- /dev/null
+++ b/src/python/grpcio/tests/_runner.py
@@ -0,0 +1,224 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import cStringIO as StringIO
+import collections
+import fcntl
+import multiprocessing
+import os
+import select
+import signal
+import sys
+import threading
+import time
+import unittest
+import uuid
+
+from tests import _loader
+from tests import _result
+
+
+class CapturePipe(object):
+ """A context-manager pipe to redirect output to a byte array.
+
+ Attributes:
+ _redirect_fd (int): File descriptor of file to redirect writes from.
+ _saved_fd (int): A copy of the original value of the redirected file
+ descriptor.
+ _read_thread (threading.Thread or None): Thread upon which reads through the
+ pipe are performed. Only non-None when self is started.
+ _read_fd (int or None): File descriptor of the read end of the redirect
+ pipe. Only non-None when self is started.
+ _write_fd (int or None): File descriptor of the write end of the redirect
+ pipe. Only non-None when self is started.
+ output (bytearray or None): Redirected output from writes to the redirected
+ file descriptor. Only valid during and after self has started.
+ """
+
+ def __init__(self, fd):
+ self._redirect_fd = fd
+ self._saved_fd = os.dup(self._redirect_fd)
+ self._read_thread = None
+ self._read_fd = None
+ self._write_fd = None
+ self.output = None
+
+ def start(self):
+ """Start redirection of writes to the file descriptor."""
+ self._read_fd, self._write_fd = os.pipe()
+ os.dup2(self._write_fd, self._redirect_fd)
+ flags = fcntl.fcntl(self._read_fd, fcntl.F_GETFL)
+ fcntl.fcntl(self._read_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+ self._read_thread = threading.Thread(target=self._read)
+ self._read_thread.start()
+
+ def stop(self):
+ """Stop redirection of writes to the file descriptor."""
+ os.close(self._write_fd)
+ os.dup2(self._saved_fd, self._redirect_fd) # auto-close self._redirect_fd
+ self._read_thread.join()
+ self._read_thread = None
+ # we waited for the read thread to finish, so _read_fd has been read and we
+ # can close it.
+ os.close(self._read_fd)
+
+ def _read(self):
+ """Read-thread target for self."""
+ self.output = bytearray()
+ while True:
+ select.select([self._read_fd], [], [])
+ read_bytes = os.read(self._read_fd, 1024)
+ if read_bytes:
+ self.output.extend(read_bytes)
+ else:
+ break
+
+ def write_bypass(self, value):
+ """Bypass the redirection and write directly to the original file.
+
+ Arguments:
+ value (str): What to write to the original file.
+ """
+ if self._saved_fd is None:
+ os.write(self._redirect_fd, value)
+ else:
+ os.write(self._saved_fd, value)
+
+ def __enter__(self):
+ self.start()
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.stop()
+
+ def close(self):
+ """Close any resources used by self not closed by stop()."""
+ os.close(self._saved_fd)
+
+
+class AugmentedCase(collections.namedtuple('AugmentedCase', [
+ 'case', 'id'])):
+ """A test case with a guaranteed unique externally specified identifier.
+
+ Attributes:
+ case (unittest.TestCase): TestCase we're decorating with an additional
+ identifier.
+ id (object): Any identifier that may be considered 'unique' for testing
+ purposes.
+ """
+
+ def __new__(cls, case, id=None):
+ if id is None:
+ id = uuid.uuid4()
+ return super(cls, AugmentedCase).__new__(cls, case, id)
+
+
+class Runner(object):
+
+ def run(self, suite):
+ """See setuptools' test_runner setup argument for information."""
+ # Ensure that every test case has no collision with any other test case in
+ # the augmented results.
+ augmented_cases = [AugmentedCase(case, uuid.uuid4())
+ for case in _loader.iterate_suite_cases(suite)]
+ case_id_by_case = dict((augmented_case.case, augmented_case.id)
+ for augmented_case in augmented_cases)
+ result_out = StringIO.StringIO()
+ result = _result.TerminalResult(
+ result_out, id_map=lambda case: case_id_by_case[case])
+ stdout_pipe = CapturePipe(sys.stdout.fileno())
+ stderr_pipe = CapturePipe(sys.stderr.fileno())
+ kill_flag = [False]
+
+ def sigint_handler(signal_number, frame):
+ if signal_number == signal.SIGINT:
+ kill_flag[0] = True # Python 2.7 not having 'local'... :-(
+ signal.signal(signal_number, signal.SIG_DFL)
+
+ def fault_handler(signal_number, frame):
+ stdout_pipe.write_bypass(
+ 'Received fault signal {}\nstdout:\n{}\n\nstderr:{}\n'
+ .format(signal_number, stdout_pipe.output, stderr_pipe.output))
+ os._exit(1)
+
+ def check_kill_self():
+ if kill_flag[0]:
+ stdout_pipe.write_bypass('Stopping tests short...')
+ result.stopTestRun()
+ stdout_pipe.write_bypass(result_out.getvalue())
+ stdout_pipe.write_bypass(
+ '\ninterrupted stdout:\n{}\n'.format(stdout_pipe.output))
+ stderr_pipe.write_bypass(
+ '\ninterrupted stderr:\n{}\n'.format(stderr_pipe.output))
+ os._exit(1)
+ signal.signal(signal.SIGINT, sigint_handler)
+ signal.signal(signal.SIGSEGV, fault_handler)
+ signal.signal(signal.SIGBUS, fault_handler)
+ signal.signal(signal.SIGABRT, fault_handler)
+ signal.signal(signal.SIGFPE, fault_handler)
+ signal.signal(signal.SIGILL, fault_handler)
+ # Sometimes output will lag after a test has successfully finished; we
+ # ignore such writes to our pipes.
+ signal.signal(signal.SIGPIPE, signal.SIG_IGN)
+
+ # Run the tests
+ result.startTestRun()
+ for augmented_case in augmented_cases:
+ sys.stdout.write('Running {}\n'.format(augmented_case.case.id()))
+ sys.stdout.flush()
+ case_thread = threading.Thread(
+ target=augmented_case.case.run, args=(result,))
+ try:
+ with stdout_pipe, stderr_pipe:
+ case_thread.start()
+ while case_thread.is_alive():
+ check_kill_self()
+ time.sleep(0)
+ case_thread.join()
+ except:
+ # re-raise the exception after forcing the with-block to end
+ raise
+ result.set_output(
+ augmented_case.case, stdout_pipe.output, stderr_pipe.output)
+ sys.stdout.write(result_out.getvalue())
+ sys.stdout.flush()
+ result_out.truncate(0)
+ check_kill_self()
+ result.stopTestRun()
+ stdout_pipe.close()
+ stderr_pipe.close()
+
+ # Report results
+ sys.stdout.write(result_out.getvalue())
+ sys.stdout.flush()
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ with open('report.xml', 'w') as report_xml_file:
+ _result.jenkins_junit_xml(result).write(report_xml_file)
+ return result
+
diff --git a/src/python/grpcio/tests/interop/__init__.py b/src/python/grpcio/tests/interop/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/interop/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/interop/_insecure_interop_test.py b/src/python/grpcio/tests/interop/_insecure_interop_test.py
new file mode 100644
index 0000000000..00b49aba37
--- /dev/null
+++ b/src/python/grpcio/tests/interop/_insecure_interop_test.py
@@ -0,0 +1,58 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Insecure client-server interoperability as a unit test."""
+
+import unittest
+
+from grpc.beta import implementations
+
+from tests.interop import _interop_test_case
+from tests.interop import methods
+from tests.interop import server
+from tests.interop import test_pb2
+
+
+class InsecureInteropTest(
+ _interop_test_case.InteropTestCase,
+ unittest.TestCase):
+
+ def setUp(self):
+ self.server = test_pb2.beta_create_TestService_server(methods.TestService())
+ port = self.server.add_insecure_port('[::]:0')
+ self.server.start()
+ self.stub = test_pb2.beta_create_TestService_stub(
+ implementations.insecure_channel('[::]', port))
+
+ def tearDown(self):
+ self.server.stop(0)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/interop/_interop_test_case.py b/src/python/grpcio/tests/interop/_interop_test_case.py
new file mode 100644
index 0000000000..ccea17a66d
--- /dev/null
+++ b/src/python/grpcio/tests/interop/_interop_test_case.py
@@ -0,0 +1,64 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Common code for unit tests of the interoperability test code."""
+
+from tests.interop import methods
+
+
+class InteropTestCase(object):
+ """Unit test methods.
+
+ This class must be mixed in with unittest.TestCase and a class that defines
+ setUp and tearDown methods that manage a stub attribute.
+ """
+
+ def testEmptyUnary(self):
+ methods.TestCase.EMPTY_UNARY.test_interoperability(self.stub, None)
+
+ def testLargeUnary(self):
+ methods.TestCase.LARGE_UNARY.test_interoperability(self.stub, None)
+
+ def testServerStreaming(self):
+ methods.TestCase.SERVER_STREAMING.test_interoperability(self.stub, None)
+
+ def testClientStreaming(self):
+ methods.TestCase.CLIENT_STREAMING.test_interoperability(self.stub, None)
+
+ def testPingPong(self):
+ methods.TestCase.PING_PONG.test_interoperability(self.stub, None)
+
+ def testCancelAfterBegin(self):
+ methods.TestCase.CANCEL_AFTER_BEGIN.test_interoperability(self.stub, None)
+
+ def testCancelAfterFirstResponse(self):
+ methods.TestCase.CANCEL_AFTER_FIRST_RESPONSE.test_interoperability(self.stub, None)
+
+ def testTimeoutOnSleepingServer(self):
+ methods.TestCase.TIMEOUT_ON_SLEEPING_SERVER.test_interoperability(self.stub, None)
diff --git a/src/python/grpcio/tests/interop/_secure_interop_test.py b/src/python/grpcio/tests/interop/_secure_interop_test.py
new file mode 100644
index 0000000000..7e3061133f
--- /dev/null
+++ b/src/python/grpcio/tests/interop/_secure_interop_test.py
@@ -0,0 +1,67 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Secure client-server interoperability as a unit test."""
+
+import unittest
+
+from grpc.beta import implementations
+
+from tests.interop import _interop_test_case
+from tests.interop import methods
+from tests.interop import resources
+from tests.interop import test_pb2
+
+from tests.unit.beta import test_utilities
+
+_SERVER_HOST_OVERRIDE = 'foo.test.google.fr'
+
+
+class SecureInteropTest(
+ _interop_test_case.InteropTestCase,
+ unittest.TestCase):
+
+ def setUp(self):
+ self.server = test_pb2.beta_create_TestService_server(methods.TestService())
+ port = self.server.add_secure_port(
+ '[::]:0', implementations.ssl_server_credentials(
+ [(resources.private_key(), resources.certificate_chain())]))
+ self.server.start()
+ self.stub = test_pb2.beta_create_TestService_stub(
+ test_utilities.not_really_secure_channel(
+ '[::]', port, implementations.ssl_channel_credentials(
+ resources.test_root_certificates(), None, None),
+ _SERVER_HOST_OVERRIDE))
+
+ def tearDown(self):
+ self.server.stop(0)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/interop/client.py b/src/python/grpcio/tests/interop/client.py
new file mode 100644
index 0000000000..573ec2bd71
--- /dev/null
+++ b/src/python/grpcio/tests/interop/client.py
@@ -0,0 +1,124 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""The Python implementation of the GRPC interoperability test client."""
+
+import argparse
+from oauth2client import client as oauth2client_client
+
+from grpc.beta import implementations
+
+from tests.interop import methods
+from tests.interop import resources
+from tests.interop import test_pb2
+from tests.unit.beta import test_utilities
+
+_ONE_DAY_IN_SECONDS = 60 * 60 * 24
+
+
+def _args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--server_host', help='the host to which to connect', type=str)
+ parser.add_argument(
+ '--server_port', help='the port to which to connect', type=int)
+ parser.add_argument(
+ '--test_case', help='the test case to execute', type=str)
+ parser.add_argument(
+ '--use_tls', help='require a secure connection', default=False,
+ type=resources.parse_bool)
+ parser.add_argument(
+ '--use_test_ca', help='replace platform root CAs with ca.pem',
+ default=False, type=resources.parse_bool)
+ parser.add_argument(
+ '--server_host_override',
+ help='the server host to which to claim to connect', type=str)
+ parser.add_argument('--oauth_scope', help='scope for OAuth tokens', type=str)
+ parser.add_argument(
+ '--default_service_account',
+ help='email address of the default service account', type=str)
+ return parser.parse_args()
+
+def _oauth_access_token(args):
+ credentials = oauth2client_client.GoogleCredentials.get_application_default()
+ scoped_credentials = credentials.create_scoped([args.oauth_scope])
+ return scoped_credentials.get_access_token().access_token
+
+def _stub(args):
+ if args.oauth_scope:
+ if args.test_case == 'oauth2_auth_token':
+ # TODO(jtattermusch): This testcase sets the auth metadata key-value
+ # manually, which also means that the user would need to do the same
+ # thing every time he/she would like to use and out of band oauth token.
+ # The transformer function that produces the metadata key-value from
+ # the access token should be provided by gRPC auth library.
+ access_token = _oauth_access_token(args)
+ metadata_transformer = lambda x: [
+ ('authorization', 'Bearer %s' % access_token)]
+ else:
+ metadata_transformer = lambda x: [
+ ('authorization', 'Bearer %s' % _oauth_access_token(args))]
+ else:
+ metadata_transformer = lambda x: []
+ if args.use_tls:
+ if args.use_test_ca:
+ root_certificates = resources.test_root_certificates()
+ else:
+ root_certificates = None # will load default roots.
+
+ channel = test_utilities.not_really_secure_channel(
+ args.server_host, args.server_port,
+ implementations.ssl_channel_credentials(root_certificates, None, None),
+ args.server_host_override)
+ stub = test_pb2.beta_create_TestService_stub(
+ channel, metadata_transformer=metadata_transformer)
+ else:
+ channel = implementations.insecure_channel(
+ args.server_host, args.server_port)
+ stub = test_pb2.beta_create_TestService_stub(channel)
+ return stub
+
+
+def _test_case_from_arg(test_case_arg):
+ for test_case in methods.TestCase:
+ if test_case_arg == test_case.value:
+ return test_case
+ else:
+ raise ValueError('No test case "%s"!' % test_case_arg)
+
+
+def test_interoperability():
+ args = _args()
+ stub = _stub(args)
+ test_case = _test_case_from_arg(args.test_case)
+ test_case.test_interoperability(stub, args)
+
+
+if __name__ == '__main__':
+ test_interoperability()
diff --git a/src/python/grpcio/tests/interop/credentials/README b/src/python/grpcio/tests/interop/credentials/README
new file mode 100644
index 0000000000..cb20dcb49f
--- /dev/null
+++ b/src/python/grpcio/tests/interop/credentials/README
@@ -0,0 +1 @@
+These are test keys *NOT* to be used in production.
diff --git a/src/python/grpcio/tests/interop/credentials/ca.pem b/src/python/grpcio/tests/interop/credentials/ca.pem
new file mode 100755
index 0000000000..6c8511a73c
--- /dev/null
+++ b/src/python/grpcio/tests/interop/credentials/ca.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
+Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
+YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
+BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
+g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
+Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
+sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
+oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
+Dfcog5wrJytaQ6UA0wE=
+-----END CERTIFICATE-----
diff --git a/src/python/grpcio/tests/interop/credentials/server1.key b/src/python/grpcio/tests/interop/credentials/server1.key
new file mode 100755
index 0000000000..143a5b8765
--- /dev/null
+++ b/src/python/grpcio/tests/interop/credentials/server1.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
+M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
+3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
+AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
+V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
+tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
+dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
+K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
+81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
+DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
+aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
+ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
+XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
+F98XJ7tIFfJq
+-----END PRIVATE KEY-----
diff --git a/src/python/grpcio/tests/interop/credentials/server1.pem b/src/python/grpcio/tests/interop/credentials/server1.pem
new file mode 100755
index 0000000000..f3d43fcc5b
--- /dev/null
+++ b/src/python/grpcio/tests/interop/credentials/server1.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
+MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
+dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
+MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
+BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
+ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
+LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
+zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
+9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
+CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
+em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
+CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
+hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
+y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
+-----END CERTIFICATE-----
diff --git a/src/python/grpcio/tests/interop/empty.proto b/src/python/grpcio/tests/interop/empty.proto
new file mode 100644
index 0000000000..6d0eb937d6
--- /dev/null
+++ b/src/python/grpcio/tests/interop/empty.proto
@@ -0,0 +1,43 @@
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+syntax = "proto3";
+
+package grpc.testing;
+
+// An empty message that you can re-use to avoid defining duplicated empty
+// messages in your project. A typical example is to use it as argument or the
+// return value of a service API. For instance:
+//
+// service Foo {
+// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { };
+// };
+//
+message Empty {}
diff --git a/src/python/grpcio/tests/interop/messages.proto b/src/python/grpcio/tests/interop/messages.proto
new file mode 100644
index 0000000000..193b6c4171
--- /dev/null
+++ b/src/python/grpcio/tests/interop/messages.proto
@@ -0,0 +1,167 @@
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Message definitions to be used by integration test service definitions.
+
+syntax = "proto3";
+
+package grpc.testing;
+
+// The type of payload that should be returned.
+enum PayloadType {
+ // Compressable text format.
+ COMPRESSABLE = 0;
+
+ // Uncompressable binary format.
+ UNCOMPRESSABLE = 1;
+
+ // Randomly chosen from all other formats defined in this enum.
+ RANDOM = 2;
+}
+
+// Compression algorithms
+enum CompressionType {
+ // No compression
+ NONE = 0;
+ GZIP = 1;
+ DEFLATE = 2;
+}
+
+// A block of data, to simply increase gRPC message size.
+message Payload {
+ // The type of data in body.
+ PayloadType type = 1;
+ // Primary contents of payload.
+ bytes body = 2;
+}
+
+// A protobuf representation for grpc status. This is used by test
+// clients to specify a status that the server should attempt to return.
+message EchoStatus {
+ int32 code = 1;
+ string message = 2;
+}
+
+// Unary request.
+message SimpleRequest {
+ // Desired payload type in the response from the server.
+ // If response_type is RANDOM, server randomly chooses one from other formats.
+ PayloadType response_type = 1;
+
+ // Desired payload size in the response from the server.
+ // If response_type is COMPRESSABLE, this denotes the size before compression.
+ int32 response_size = 2;
+
+ // Optional input payload sent along with the request.
+ Payload payload = 3;
+
+ // Whether SimpleResponse should include username.
+ bool fill_username = 4;
+
+ // Whether SimpleResponse should include OAuth scope.
+ bool fill_oauth_scope = 5;
+
+ // Compression algorithm to be used by the server for the response (stream)
+ CompressionType response_compression = 6;
+
+ // Whether server should return a given status
+ EchoStatus response_status = 7;
+}
+
+// Unary response, as configured by the request.
+message SimpleResponse {
+ // Payload to increase message size.
+ Payload payload = 1;
+ // The user the request came from, for verifying authentication was
+ // successful when the client expected it.
+ string username = 2;
+ // OAuth scope.
+ string oauth_scope = 3;
+}
+
+// Client-streaming request.
+message StreamingInputCallRequest {
+ // Optional input payload sent along with the request.
+ Payload payload = 1;
+
+ // Not expecting any payload from the response.
+}
+
+// Client-streaming response.
+message StreamingInputCallResponse {
+ // Aggregated size of payloads received from the client.
+ int32 aggregated_payload_size = 1;
+}
+
+// Configuration for a particular response.
+message ResponseParameters {
+ // Desired payload sizes in responses from the server.
+ // If response_type is COMPRESSABLE, this denotes the size before compression.
+ int32 size = 1;
+
+ // Desired interval between consecutive responses in the response stream in
+ // microseconds.
+ int32 interval_us = 2;
+}
+
+// Server-streaming request.
+message StreamingOutputCallRequest {
+ // Desired payload type in the response from the server.
+ // If response_type is RANDOM, the payload from each response in the stream
+ // might be of different types. This is to simulate a mixed type of payload
+ // stream.
+ PayloadType response_type = 1;
+
+ // Configuration for each expected response message.
+ repeated ResponseParameters response_parameters = 2;
+
+ // Optional input payload sent along with the request.
+ Payload payload = 3;
+
+ // Compression algorithm to be used by the server for the response (stream)
+ CompressionType response_compression = 6;
+
+ // Whether server should return a given status
+ EchoStatus response_status = 7;
+}
+
+// Server-streaming response, as configured by the request and parameters.
+message StreamingOutputCallResponse {
+ // Payload to increase response size.
+ Payload payload = 1;
+}
+
+// For reconnect interop test only.
+// Server tells client whether its reconnects are following the spec and the
+// reconnect backoffs it saw.
+message ReconnectInfo {
+ bool passed = 1;
+ repeated int32 backoff_ms = 2;
+}
diff --git a/src/python/grpcio/tests/interop/methods.py b/src/python/grpcio/tests/interop/methods.py
new file mode 100644
index 0000000000..b3591aef7b
--- /dev/null
+++ b/src/python/grpcio/tests/interop/methods.py
@@ -0,0 +1,341 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Implementations of interoperability test methods."""
+
+import enum
+import json
+import os
+import threading
+import time
+
+from oauth2client import client as oauth2client_client
+
+from grpc.framework.common import cardinality
+from grpc.framework.interfaces.face import face
+
+from tests.interop import empty_pb2
+from tests.interop import messages_pb2
+from tests.interop import test_pb2
+
+_TIMEOUT = 7
+
+
+class TestService(test_pb2.BetaTestServiceServicer):
+
+ def EmptyCall(self, request, context):
+ return empty_pb2.Empty()
+
+ def UnaryCall(self, request, context):
+ return messages_pb2.SimpleResponse(
+ payload=messages_pb2.Payload(
+ type=messages_pb2.COMPRESSABLE,
+ body=b'\x00' * request.response_size))
+
+ def StreamingOutputCall(self, request, context):
+ for response_parameters in request.response_parameters:
+ yield messages_pb2.StreamingOutputCallResponse(
+ payload=messages_pb2.Payload(
+ type=request.response_type,
+ body=b'\x00' * response_parameters.size))
+
+ def StreamingInputCall(self, request_iterator, context):
+ aggregate_size = 0
+ for request in request_iterator:
+ if request.payload and request.payload.body:
+ aggregate_size += len(request.payload.body)
+ return messages_pb2.StreamingInputCallResponse(
+ aggregated_payload_size=aggregate_size)
+
+ def FullDuplexCall(self, request_iterator, context):
+ for request in request_iterator:
+ yield messages_pb2.StreamingOutputCallResponse(
+ payload=messages_pb2.Payload(
+ type=request.payload.type,
+ body=b'\x00' * request.response_parameters[0].size))
+
+ # NOTE(nathaniel): Apparently this is the same as the full-duplex call?
+ # NOTE(atash): It isn't even called in the interop spec (Oct 22 2015)...
+ def HalfDuplexCall(self, request_iterator, context):
+ return self.FullDuplexCall(request_iterator, context)
+
+
+def _large_unary_common_behavior(stub, fill_username, fill_oauth_scope):
+ with stub:
+ request = messages_pb2.SimpleRequest(
+ response_type=messages_pb2.COMPRESSABLE, response_size=314159,
+ payload=messages_pb2.Payload(body=b'\x00' * 271828),
+ fill_username=fill_username, fill_oauth_scope=fill_oauth_scope)
+ response_future = stub.UnaryCall.future(request, _TIMEOUT)
+ response = response_future.result()
+ if response.payload.type is not messages_pb2.COMPRESSABLE:
+ raise ValueError(
+ 'response payload type is "%s"!' % type(response.payload.type))
+ if len(response.payload.body) != 314159:
+ raise ValueError(
+ 'response body of incorrect size %d!' % len(response.payload.body))
+ return response
+
+
+def _empty_unary(stub):
+ with stub:
+ response = stub.EmptyCall(empty_pb2.Empty(), _TIMEOUT)
+ if not isinstance(response, empty_pb2.Empty):
+ raise TypeError(
+ 'response is of type "%s", not empty_pb2.Empty!', type(response))
+
+
+def _large_unary(stub):
+ _large_unary_common_behavior(stub, False, False)
+
+
+def _client_streaming(stub):
+ with stub:
+ payload_body_sizes = (27182, 8, 1828, 45904)
+ payloads = (
+ messages_pb2.Payload(body=b'\x00' * size)
+ for size in payload_body_sizes)
+ requests = (
+ messages_pb2.StreamingInputCallRequest(payload=payload)
+ for payload in payloads)
+ response = stub.StreamingInputCall(requests, _TIMEOUT)
+ if response.aggregated_payload_size != 74922:
+ raise ValueError(
+ 'incorrect size %d!' % response.aggregated_payload_size)
+
+
+def _server_streaming(stub):
+ sizes = (31415, 9, 2653, 58979)
+
+ with stub:
+ request = messages_pb2.StreamingOutputCallRequest(
+ response_type=messages_pb2.COMPRESSABLE,
+ response_parameters=(
+ messages_pb2.ResponseParameters(size=sizes[0]),
+ messages_pb2.ResponseParameters(size=sizes[1]),
+ messages_pb2.ResponseParameters(size=sizes[2]),
+ messages_pb2.ResponseParameters(size=sizes[3]),
+ ))
+ response_iterator = stub.StreamingOutputCall(request, _TIMEOUT)
+ for index, response in enumerate(response_iterator):
+ if response.payload.type != messages_pb2.COMPRESSABLE:
+ raise ValueError(
+ 'response body of invalid type %s!' % response.payload.type)
+ if len(response.payload.body) != sizes[index]:
+ raise ValueError(
+ 'response body of invalid size %d!' % len(response.payload.body))
+
+def _cancel_after_begin(stub):
+ with stub:
+ sizes = (27182, 8, 1828, 45904)
+ payloads = [messages_pb2.Payload(body=b'\x00' * size) for size in sizes]
+ requests = [messages_pb2.StreamingInputCallRequest(payload=payload)
+ for payload in payloads]
+ responses = stub.StreamingInputCall.future(requests, _TIMEOUT)
+ responses.cancel()
+ if not responses.cancelled():
+ raise ValueError('expected call to be cancelled')
+
+
+class _Pipe(object):
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._values = []
+ self._open = True
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ with self._condition:
+ while not self._values and self._open:
+ self._condition.wait()
+ if self._values:
+ return self._values.pop(0)
+ else:
+ raise StopIteration()
+
+ def add(self, value):
+ with self._condition:
+ self._values.append(value)
+ self._condition.notify()
+
+ def close(self):
+ with self._condition:
+ self._open = False
+ self._condition.notify()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.close()
+
+
+def _ping_pong(stub):
+ request_response_sizes = (31415, 9, 2653, 58979)
+ request_payload_sizes = (27182, 8, 1828, 45904)
+
+ with stub, _Pipe() as pipe:
+ response_iterator = stub.FullDuplexCall(pipe, _TIMEOUT)
+ print 'Starting ping-pong with response iterator %s' % response_iterator
+ for response_size, payload_size in zip(
+ request_response_sizes, request_payload_sizes):
+ request = messages_pb2.StreamingOutputCallRequest(
+ response_type=messages_pb2.COMPRESSABLE,
+ response_parameters=(messages_pb2.ResponseParameters(
+ size=response_size),),
+ payload=messages_pb2.Payload(body=b'\x00' * payload_size))
+ pipe.add(request)
+ response = next(response_iterator)
+ if response.payload.type != messages_pb2.COMPRESSABLE:
+ raise ValueError(
+ 'response body of invalid type %s!' % response.payload.type)
+ if len(response.payload.body) != response_size:
+ raise ValueError(
+ 'response body of invalid size %d!' % len(response.payload.body))
+
+
+def _cancel_after_first_response(stub):
+ request_response_sizes = (31415, 9, 2653, 58979)
+ request_payload_sizes = (27182, 8, 1828, 45904)
+ with stub, _Pipe() as pipe:
+ response_iterator = stub.FullDuplexCall(pipe, _TIMEOUT)
+
+ response_size = request_response_sizes[0]
+ payload_size = request_payload_sizes[0]
+ request = messages_pb2.StreamingOutputCallRequest(
+ response_type=messages_pb2.COMPRESSABLE,
+ response_parameters=(messages_pb2.ResponseParameters(
+ size=response_size),),
+ payload=messages_pb2.Payload(body=b'\x00' * payload_size))
+ pipe.add(request)
+ response = next(response_iterator)
+ # We test the contents of `response` in the Ping Pong test - don't check
+ # them here.
+ response_iterator.cancel()
+
+ try:
+ next(response_iterator)
+ except Exception:
+ pass
+ else:
+ raise ValueError('expected call to be cancelled')
+
+
+def _timeout_on_sleeping_server(stub):
+ request_payload_size = 27182
+ with stub, _Pipe() as pipe:
+ response_iterator = stub.FullDuplexCall(pipe, 0.001)
+
+ request = messages_pb2.StreamingOutputCallRequest(
+ response_type=messages_pb2.COMPRESSABLE,
+ payload=messages_pb2.Payload(body=b'\x00' * request_payload_size))
+ pipe.add(request)
+ time.sleep(0.1)
+ try:
+ next(response_iterator)
+ except face.ExpirationError:
+ pass
+ else:
+ raise ValueError('expected call to exceed deadline')
+
+
+def _empty_stream(stub):
+ with stub, _Pipe() as pipe:
+ response_iterator = stub.FullDuplexCall(pipe, _TIMEOUT)
+ pipe.close()
+ try:
+ next(response_iterator)
+ raise ValueError('expected exactly 0 responses')
+ except StopIteration:
+ pass
+
+
+def _compute_engine_creds(stub, args):
+ response = _large_unary_common_behavior(stub, True, True)
+ if args.default_service_account != response.username:
+ raise ValueError(
+ 'expected username %s, got %s' % (args.default_service_account,
+ response.username))
+
+
+def _oauth2_auth_token(stub, args):
+ json_key_filename = os.environ[
+ oauth2client_client.GOOGLE_APPLICATION_CREDENTIALS]
+ wanted_email = json.load(open(json_key_filename, 'rb'))['client_email']
+ response = _large_unary_common_behavior(stub, True, True)
+ if wanted_email != response.username:
+ raise ValueError(
+ 'expected username %s, got %s' % (wanted_email, response.username))
+ if args.oauth_scope.find(response.oauth_scope) == -1:
+ raise ValueError(
+ 'expected to find oauth scope "%s" in received "%s"' %
+ (response.oauth_scope, args.oauth_scope))
+
+@enum.unique
+class TestCase(enum.Enum):
+ EMPTY_UNARY = 'empty_unary'
+ LARGE_UNARY = 'large_unary'
+ SERVER_STREAMING = 'server_streaming'
+ CLIENT_STREAMING = 'client_streaming'
+ PING_PONG = 'ping_pong'
+ CANCEL_AFTER_BEGIN = 'cancel_after_begin'
+ CANCEL_AFTER_FIRST_RESPONSE = 'cancel_after_first_response'
+ EMPTY_STREAM = 'empty_stream'
+ COMPUTE_ENGINE_CREDS = 'compute_engine_creds'
+ OAUTH2_AUTH_TOKEN = 'oauth2_auth_token'
+ TIMEOUT_ON_SLEEPING_SERVER = 'timeout_on_sleeping_server'
+
+ def test_interoperability(self, stub, args):
+ if self is TestCase.EMPTY_UNARY:
+ _empty_unary(stub)
+ elif self is TestCase.LARGE_UNARY:
+ _large_unary(stub)
+ elif self is TestCase.SERVER_STREAMING:
+ _server_streaming(stub)
+ elif self is TestCase.CLIENT_STREAMING:
+ _client_streaming(stub)
+ elif self is TestCase.PING_PONG:
+ _ping_pong(stub)
+ elif self is TestCase.CANCEL_AFTER_BEGIN:
+ _cancel_after_begin(stub)
+ elif self is TestCase.CANCEL_AFTER_FIRST_RESPONSE:
+ _cancel_after_first_response(stub)
+ elif self is TestCase.TIMEOUT_ON_SLEEPING_SERVER:
+ _timeout_on_sleeping_server(stub)
+ elif self is TestCase.EMPTY_STREAM:
+ _empty_stream(stub)
+ elif self is TestCase.COMPUTE_ENGINE_CREDS:
+ _compute_engine_creds(stub, args)
+ elif self is TestCase.OAUTH2_AUTH_TOKEN:
+ _oauth2_auth_token(stub, args)
+ else:
+ raise NotImplementedError('Test case "%s" not implemented!' % self.name)
diff --git a/src/python/grpcio/tests/interop/resources.py b/src/python/grpcio/tests/interop/resources.py
new file mode 100644
index 0000000000..c424385cf6
--- /dev/null
+++ b/src/python/grpcio/tests/interop/resources.py
@@ -0,0 +1,61 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Constants and functions for data used in interoperability testing."""
+
+import argparse
+import os
+
+import pkg_resources
+
+_ROOT_CERTIFICATES_RESOURCE_PATH = 'credentials/ca.pem'
+_PRIVATE_KEY_RESOURCE_PATH = 'credentials/server1.key'
+_CERTIFICATE_CHAIN_RESOURCE_PATH = 'credentials/server1.pem'
+
+
+def test_root_certificates():
+ return pkg_resources.resource_string(
+ __name__, _ROOT_CERTIFICATES_RESOURCE_PATH)
+
+
+def private_key():
+ return pkg_resources.resource_string(__name__, _PRIVATE_KEY_RESOURCE_PATH)
+
+
+def certificate_chain():
+ return pkg_resources.resource_string(
+ __name__, _CERTIFICATE_CHAIN_RESOURCE_PATH)
+
+
+def parse_bool(value):
+ if value == 'true':
+ return True
+ if value == 'false':
+ return False
+ raise argparse.ArgumentTypeError('Only true/false allowed')
diff --git a/src/python/grpcio/tests/interop/server.py b/src/python/grpcio/tests/interop/server.py
new file mode 100644
index 0000000000..6dd55f008c
--- /dev/null
+++ b/src/python/grpcio/tests/interop/server.py
@@ -0,0 +1,75 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""The Python implementation of the GRPC interoperability test server."""
+
+import argparse
+import logging
+import time
+
+from grpc.beta import implementations
+
+from tests.interop import methods
+from tests.interop import resources
+from tests.interop import test_pb2
+
+_ONE_DAY_IN_SECONDS = 60 * 60 * 24
+
+
+def serve():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--port', help='the port on which to serve', type=int)
+ parser.add_argument(
+ '--use_tls', help='require a secure connection',
+ default=False, type=resources.parse_bool)
+ args = parser.parse_args()
+
+ server = test_pb2.beta_create_TestService_server(methods.TestService())
+ if args.use_tls:
+ private_key = resources.private_key()
+ certificate_chain = resources.certificate_chain()
+ credentials = implementations.ssl_server_credentials(
+ [(private_key, certificate_chain)])
+ server.add_secure_port('[::]:{}'.format(args.port), credentials)
+ else:
+ server.add_insecure_port('[::]:{}'.format(args.port))
+
+ server.start()
+ logging.info('Server serving.')
+ try:
+ while True:
+ time.sleep(_ONE_DAY_IN_SECONDS)
+ except BaseException as e:
+ logging.info('Caught exception "%s"; stopping server...', e)
+ server.stop(0)
+ logging.info('Server stopped; exiting.')
+
+if __name__ == '__main__':
+ serve()
diff --git a/src/python/grpcio/tests/interop/test.proto b/src/python/grpcio/tests/interop/test.proto
new file mode 100644
index 0000000000..9feecc0278
--- /dev/null
+++ b/src/python/grpcio/tests/interop/test.proto
@@ -0,0 +1,86 @@
+
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// An integration test service that covers all the method signature permutations
+// of unary/streaming requests/responses.
+
+syntax = "proto3";
+
+import "tests/interop/empty.proto";
+import "tests/interop/messages.proto";
+
+package grpc.testing;
+
+// A simple service to test the various types of RPCs and experiment with
+// performance with various types of payload.
+service TestService {
+ // One empty request followed by one empty response.
+ rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty);
+
+ // One request followed by one response.
+ rpc UnaryCall(SimpleRequest) returns (SimpleResponse);
+
+ // One request followed by a sequence of responses (streamed download).
+ // The server returns the payload with client desired type and sizes.
+ rpc StreamingOutputCall(StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+
+ // A sequence of requests followed by one response (streamed upload).
+ // The server returns the aggregated size of client payload as the result.
+ rpc StreamingInputCall(stream StreamingInputCallRequest)
+ returns (StreamingInputCallResponse);
+
+ // A sequence of requests with each request served by the server immediately.
+ // As one request could lead to multiple responses, this interface
+ // demonstrates the idea of full duplexing.
+ rpc FullDuplexCall(stream StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+
+ // A sequence of requests followed by a sequence of responses.
+ // The server buffers all the client requests and then serves them in order. A
+ // stream of responses are returned to the client when the server starts with
+ // first request.
+ rpc HalfDuplexCall(stream StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+}
+
+
+// A simple service NOT implemented at servers so clients can test for
+// that case.
+service UnimplementedService {
+ // A call that no server should implement
+ rpc UnimplementedCall(grpc.testing.Empty) returns(grpc.testing.Empty);
+}
+
+// A service used to control reconnect server.
+service ReconnectService {
+ rpc Start(grpc.testing.Empty) returns (grpc.testing.Empty);
+ rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo);
+}
diff --git a/src/python/grpcio/tests/protoc_plugin/__init__.py b/src/python/grpcio/tests/protoc_plugin/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/protoc_plugin/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/protoc_plugin/beta_python_plugin_test.py b/src/python/grpcio/tests/protoc_plugin/beta_python_plugin_test.py
new file mode 100644
index 0000000000..ba5b219a88
--- /dev/null
+++ b/src/python/grpcio/tests/protoc_plugin/beta_python_plugin_test.py
@@ -0,0 +1,524 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import argparse
+import contextlib
+import distutils.spawn
+import errno
+import itertools
+import os
+import pkg_resources
+import shutil
+import subprocess
+import sys
+import tempfile
+import threading
+import time
+import unittest
+
+from grpc.beta import implementations
+from grpc.framework.foundation import future
+from grpc.framework.interfaces.face import face
+from tests.unit.framework.common import test_constants
+
+# Identifiers of entities we expect to find in the generated module.
+SERVICER_IDENTIFIER = 'BetaTestServiceServicer'
+STUB_IDENTIFIER = 'BetaTestServiceStub'
+SERVER_FACTORY_IDENTIFIER = 'beta_create_TestService_server'
+STUB_FACTORY_IDENTIFIER = 'beta_create_TestService_stub'
+
+
+class _ServicerMethods(object):
+
+ def __init__(self, test_pb2):
+ self._condition = threading.Condition()
+ self._paused = False
+ self._fail = False
+ self._test_pb2 = test_pb2
+
+ @contextlib.contextmanager
+ def pause(self): # pylint: disable=invalid-name
+ with self._condition:
+ self._paused = True
+ yield
+ with self._condition:
+ self._paused = False
+ self._condition.notify_all()
+
+ @contextlib.contextmanager
+ def fail(self): # pylint: disable=invalid-name
+ with self._condition:
+ self._fail = True
+ yield
+ with self._condition:
+ self._fail = False
+
+ def _control(self): # pylint: disable=invalid-name
+ with self._condition:
+ if self._fail:
+ raise ValueError()
+ while self._paused:
+ self._condition.wait()
+
+ def UnaryCall(self, request, unused_rpc_context):
+ response = self._test_pb2.SimpleResponse()
+ response.payload.payload_type = self._test_pb2.COMPRESSABLE
+ response.payload.payload_compressable = 'a' * request.response_size
+ self._control()
+ return response
+
+ def StreamingOutputCall(self, request, unused_rpc_context):
+ for parameter in request.response_parameters:
+ response = self._test_pb2.StreamingOutputCallResponse()
+ response.payload.payload_type = self._test_pb2.COMPRESSABLE
+ response.payload.payload_compressable = 'a' * parameter.size
+ self._control()
+ yield response
+
+ def StreamingInputCall(self, request_iter, unused_rpc_context):
+ response = self._test_pb2.StreamingInputCallResponse()
+ aggregated_payload_size = 0
+ for request in request_iter:
+ aggregated_payload_size += len(request.payload.payload_compressable)
+ response.aggregated_payload_size = aggregated_payload_size
+ self._control()
+ return response
+
+ def FullDuplexCall(self, request_iter, unused_rpc_context):
+ for request in request_iter:
+ for parameter in request.response_parameters:
+ response = self._test_pb2.StreamingOutputCallResponse()
+ response.payload.payload_type = self._test_pb2.COMPRESSABLE
+ response.payload.payload_compressable = 'a' * parameter.size
+ self._control()
+ yield response
+
+ def HalfDuplexCall(self, request_iter, unused_rpc_context):
+ responses = []
+ for request in request_iter:
+ for parameter in request.response_parameters:
+ response = self._test_pb2.StreamingOutputCallResponse()
+ response.payload.payload_type = self._test_pb2.COMPRESSABLE
+ response.payload.payload_compressable = 'a' * parameter.size
+ self._control()
+ responses.append(response)
+ for response in responses:
+ yield response
+
+
+@contextlib.contextmanager
+def _CreateService(test_pb2):
+ """Provides a servicer backend and a stub.
+
+ The servicer is just the implementation of the actual servicer passed to the
+ face player of the python RPC implementation; the two are detached.
+
+ Args:
+ test_pb2: The test_pb2 module generated by this test.
+
+ Yields:
+ A (servicer_methods, stub) pair where servicer_methods is the back-end of
+ the service bound to the stub and and stub is the stub on which to invoke
+ RPCs.
+ """
+ servicer_methods = _ServicerMethods(test_pb2)
+
+ class Servicer(getattr(test_pb2, SERVICER_IDENTIFIER)):
+
+ def UnaryCall(self, request, context):
+ return servicer_methods.UnaryCall(request, context)
+
+ def StreamingOutputCall(self, request, context):
+ return servicer_methods.StreamingOutputCall(request, context)
+
+ def StreamingInputCall(self, request_iter, context):
+ return servicer_methods.StreamingInputCall(request_iter, context)
+
+ def FullDuplexCall(self, request_iter, context):
+ return servicer_methods.FullDuplexCall(request_iter, context)
+
+ def HalfDuplexCall(self, request_iter, context):
+ return servicer_methods.HalfDuplexCall(request_iter, context)
+
+ servicer = Servicer()
+ server = getattr(test_pb2, SERVER_FACTORY_IDENTIFIER)(servicer)
+ port = server.add_insecure_port('[::]:0')
+ server.start()
+ channel = implementations.insecure_channel('localhost', port)
+ stub = getattr(test_pb2, STUB_FACTORY_IDENTIFIER)(channel)
+ yield servicer_methods, stub
+ server.stop(0)
+
+
+def _streaming_input_request_iterator(test_pb2):
+ for _ in range(3):
+ request = test_pb2.StreamingInputCallRequest()
+ request.payload.payload_type = test_pb2.COMPRESSABLE
+ request.payload.payload_compressable = 'a'
+ yield request
+
+
+def _streaming_output_request(test_pb2):
+ request = test_pb2.StreamingOutputCallRequest()
+ sizes = [1, 2, 3]
+ request.response_parameters.add(size=sizes[0], interval_us=0)
+ request.response_parameters.add(size=sizes[1], interval_us=0)
+ request.response_parameters.add(size=sizes[2], interval_us=0)
+ return request
+
+
+def _full_duplex_request_iterator(test_pb2):
+ request = test_pb2.StreamingOutputCallRequest()
+ request.response_parameters.add(size=1, interval_us=0)
+ yield request
+ request = test_pb2.StreamingOutputCallRequest()
+ request.response_parameters.add(size=2, interval_us=0)
+ request.response_parameters.add(size=3, interval_us=0)
+ yield request
+
+
+class PythonPluginTest(unittest.TestCase):
+ """Test case for the gRPC Python protoc-plugin.
+
+ While reading these tests, remember that the futures API
+ (`stub.method.future()`) only gives futures for the *response-unary*
+ methods and does not exist for response-streaming methods.
+ """
+
+ def setUp(self):
+ # Assume that the appropriate protoc and grpc_python_plugins are on the
+ # path.
+ protoc_command = 'protoc'
+ protoc_plugin_filename = distutils.spawn.find_executable(
+ 'grpc_python_plugin')
+ test_proto_filename = pkg_resources.resource_filename(
+ 'tests.protoc_plugin', 'protoc_plugin_test.proto')
+ if not os.path.isfile(protoc_command):
+ # Assume that if we haven't built protoc that it's on the system.
+ protoc_command = 'protoc'
+
+ # Ensure that the output directory exists.
+ self.outdir = tempfile.mkdtemp()
+
+ # Invoke protoc with the plugin.
+ cmd = [
+ protoc_command,
+ '--plugin=protoc-gen-python-grpc=%s' % protoc_plugin_filename,
+ '-I .',
+ '--python_out=%s' % self.outdir,
+ '--python-grpc_out=%s' % self.outdir,
+ os.path.basename(test_proto_filename),
+ ]
+ subprocess.check_call(' '.join(cmd), shell=True, env=os.environ,
+ cwd=os.path.dirname(test_proto_filename))
+ sys.path.insert(0, self.outdir)
+
+ def tearDown(self):
+ try:
+ shutil.rmtree(self.outdir)
+ except OSError as exc:
+ if exc.errno != errno.ENOENT:
+ raise
+ sys.path.remove(self.outdir)
+
+ def testImportAttributes(self):
+ # check that we can access the generated module and its members.
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ self.assertIsNotNone(getattr(test_pb2, SERVICER_IDENTIFIER, None))
+ self.assertIsNotNone(getattr(test_pb2, STUB_IDENTIFIER, None))
+ self.assertIsNotNone(getattr(test_pb2, SERVER_FACTORY_IDENTIFIER, None))
+ self.assertIsNotNone(getattr(test_pb2, STUB_FACTORY_IDENTIFIER, None))
+
+ def testUpDown(self):
+ import protoc_plugin_test_pb2 as test_pb2
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (servicer, stub):
+ request = test_pb2.SimpleRequest(response_size=13)
+
+ def testUnaryCall(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ request = test_pb2.SimpleRequest(response_size=13)
+ response = stub.UnaryCall(request, test_constants.LONG_TIMEOUT)
+ expected_response = methods.UnaryCall(request, 'not a real context!')
+ self.assertEqual(expected_response, response)
+
+ def testUnaryCallFuture(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request = test_pb2.SimpleRequest(response_size=13)
+ with _CreateService(test_pb2) as (methods, stub):
+ # Check that the call does not block waiting for the server to respond.
+ with methods.pause():
+ response_future = stub.UnaryCall.future(
+ request, test_constants.LONG_TIMEOUT)
+ response = response_future.result()
+ expected_response = methods.UnaryCall(request, 'not a real RpcContext!')
+ self.assertEqual(expected_response, response)
+
+ def testUnaryCallFutureExpired(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ request = test_pb2.SimpleRequest(response_size=13)
+ with methods.pause():
+ response_future = stub.UnaryCall.future(
+ request, test_constants.SHORT_TIMEOUT)
+ with self.assertRaises(face.ExpirationError):
+ response_future.result()
+
+ def testUnaryCallFutureCancelled(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request = test_pb2.SimpleRequest(response_size=13)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.pause():
+ response_future = stub.UnaryCall.future(request, 1)
+ response_future.cancel()
+ self.assertTrue(response_future.cancelled())
+
+ def testUnaryCallFutureFailed(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request = test_pb2.SimpleRequest(response_size=13)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.fail():
+ response_future = stub.UnaryCall.future(
+ request, test_constants.LONG_TIMEOUT)
+ self.assertIsNotNone(response_future.exception())
+
+ def testStreamingOutputCall(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request = _streaming_output_request(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ responses = stub.StreamingOutputCall(
+ request, test_constants.LONG_TIMEOUT)
+ expected_responses = methods.StreamingOutputCall(
+ request, 'not a real RpcContext!')
+ for expected_response, response in itertools.izip_longest(
+ expected_responses, responses):
+ self.assertEqual(expected_response, response)
+
+ def testStreamingOutputCallExpired(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request = _streaming_output_request(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.pause():
+ responses = stub.StreamingOutputCall(
+ request, test_constants.SHORT_TIMEOUT)
+ with self.assertRaises(face.ExpirationError):
+ list(responses)
+
+ def testStreamingOutputCallCancelled(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request = _streaming_output_request(test_pb2)
+ with _CreateService(test_pb2) as (unused_methods, stub):
+ responses = stub.StreamingOutputCall(
+ request, test_constants.LONG_TIMEOUT)
+ next(responses)
+ responses.cancel()
+ with self.assertRaises(face.CancellationError):
+ next(responses)
+
+ def testStreamingOutputCallFailed(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request = _streaming_output_request(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.fail():
+ responses = stub.StreamingOutputCall(request, 1)
+ self.assertIsNotNone(responses)
+ with self.assertRaises(face.RemoteError):
+ next(responses)
+
+ def testStreamingInputCall(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ response = stub.StreamingInputCall(
+ _streaming_input_request_iterator(test_pb2),
+ test_constants.LONG_TIMEOUT)
+ expected_response = methods.StreamingInputCall(
+ _streaming_input_request_iterator(test_pb2), 'not a real RpcContext!')
+ self.assertEqual(expected_response, response)
+
+ def testStreamingInputCallFuture(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.pause():
+ response_future = stub.StreamingInputCall.future(
+ _streaming_input_request_iterator(test_pb2),
+ test_constants.LONG_TIMEOUT)
+ response = response_future.result()
+ expected_response = methods.StreamingInputCall(
+ _streaming_input_request_iterator(test_pb2), 'not a real RpcContext!')
+ self.assertEqual(expected_response, response)
+
+ def testStreamingInputCallFutureExpired(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.pause():
+ response_future = stub.StreamingInputCall.future(
+ _streaming_input_request_iterator(test_pb2),
+ test_constants.SHORT_TIMEOUT)
+ with self.assertRaises(face.ExpirationError):
+ response_future.result()
+ self.assertIsInstance(
+ response_future.exception(), face.ExpirationError)
+
+ def testStreamingInputCallFutureCancelled(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.pause():
+ response_future = stub.StreamingInputCall.future(
+ _streaming_input_request_iterator(test_pb2),
+ test_constants.LONG_TIMEOUT)
+ response_future.cancel()
+ self.assertTrue(response_future.cancelled())
+ with self.assertRaises(future.CancelledError):
+ response_future.result()
+
+ def testStreamingInputCallFutureFailed(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.fail():
+ response_future = stub.StreamingInputCall.future(
+ _streaming_input_request_iterator(test_pb2),
+ test_constants.LONG_TIMEOUT)
+ self.assertIsNotNone(response_future.exception())
+
+ def testFullDuplexCall(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ responses = stub.FullDuplexCall(
+ _full_duplex_request_iterator(test_pb2), test_constants.LONG_TIMEOUT)
+ expected_responses = methods.FullDuplexCall(
+ _full_duplex_request_iterator(test_pb2), 'not a real RpcContext!')
+ for expected_response, response in itertools.izip_longest(
+ expected_responses, responses):
+ self.assertEqual(expected_response, response)
+
+ def testFullDuplexCallExpired(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request_iterator = _full_duplex_request_iterator(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.pause():
+ responses = stub.FullDuplexCall(
+ request_iterator, test_constants.SHORT_TIMEOUT)
+ with self.assertRaises(face.ExpirationError):
+ list(responses)
+
+ def testFullDuplexCallCancelled(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ request_iterator = _full_duplex_request_iterator(test_pb2)
+ responses = stub.FullDuplexCall(
+ request_iterator, test_constants.LONG_TIMEOUT)
+ next(responses)
+ responses.cancel()
+ with self.assertRaises(face.CancellationError):
+ next(responses)
+
+ def testFullDuplexCallFailed(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ request_iterator = _full_duplex_request_iterator(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ with methods.fail():
+ responses = stub.FullDuplexCall(
+ request_iterator, test_constants.LONG_TIMEOUT)
+ self.assertIsNotNone(responses)
+ with self.assertRaises(face.RemoteError):
+ next(responses)
+
+ def testHalfDuplexCall(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ with _CreateService(test_pb2) as (methods, stub):
+ def half_duplex_request_iterator():
+ request = test_pb2.StreamingOutputCallRequest()
+ request.response_parameters.add(size=1, interval_us=0)
+ yield request
+ request = test_pb2.StreamingOutputCallRequest()
+ request.response_parameters.add(size=2, interval_us=0)
+ request.response_parameters.add(size=3, interval_us=0)
+ yield request
+ responses = stub.HalfDuplexCall(
+ half_duplex_request_iterator(), test_constants.LONG_TIMEOUT)
+ expected_responses = methods.HalfDuplexCall(
+ half_duplex_request_iterator(), 'not a real RpcContext!')
+ for check in itertools.izip_longest(expected_responses, responses):
+ expected_response, response = check
+ self.assertEqual(expected_response, response)
+
+ def testHalfDuplexCallWedged(self):
+ import protoc_plugin_test_pb2 as test_pb2 # pylint: disable=g-import-not-at-top
+ reload(test_pb2)
+ condition = threading.Condition()
+ wait_cell = [False]
+ @contextlib.contextmanager
+ def wait(): # pylint: disable=invalid-name
+ # Where's Python 3's 'nonlocal' statement when you need it?
+ with condition:
+ wait_cell[0] = True
+ yield
+ with condition:
+ wait_cell[0] = False
+ condition.notify_all()
+ def half_duplex_request_iterator():
+ request = test_pb2.StreamingOutputCallRequest()
+ request.response_parameters.add(size=1, interval_us=0)
+ yield request
+ with condition:
+ while wait_cell[0]:
+ condition.wait()
+ with _CreateService(test_pb2) as (methods, stub):
+ with wait():
+ responses = stub.HalfDuplexCall(
+ half_duplex_request_iterator(), test_constants.SHORT_TIMEOUT)
+ # half-duplex waits for the client to send all info
+ with self.assertRaises(face.ExpirationError):
+ next(responses)
+
+
+if __name__ == '__main__':
+ os.chdir(os.path.dirname(sys.argv[0]))
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/protoc_plugin/protoc_plugin_test.proto b/src/python/grpcio/tests/protoc_plugin/protoc_plugin_test.proto
new file mode 100644
index 0000000000..6762a8e7f3
--- /dev/null
+++ b/src/python/grpcio/tests/protoc_plugin/protoc_plugin_test.proto
@@ -0,0 +1,139 @@
+// Copyright 2015, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// An integration test service that covers all the method signature permutations
+// of unary/streaming requests/responses.
+// This file is duplicated around the code base. See GitHub issue #526.
+syntax = "proto2";
+
+package grpc_protoc_plugin;
+
+enum PayloadType {
+ // Compressable text format.
+ COMPRESSABLE= 1;
+
+ // Uncompressable binary format.
+ UNCOMPRESSABLE = 2;
+
+ // Randomly chosen from all other formats defined in this enum.
+ RANDOM = 3;
+}
+
+message Payload {
+ required PayloadType payload_type = 1;
+ oneof payload_body {
+ string payload_compressable = 2;
+ bytes payload_uncompressable = 3;
+ }
+}
+
+message SimpleRequest {
+ // Desired payload type in the response from the server.
+ // If response_type is RANDOM, server randomly chooses one from other formats.
+ optional PayloadType response_type = 1 [default=COMPRESSABLE];
+
+ // Desired payload size in the response from the server.
+ // If response_type is COMPRESSABLE, this denotes the size before compression.
+ optional int32 response_size = 2;
+
+ // Optional input payload sent along with the request.
+ optional Payload payload = 3;
+}
+
+message SimpleResponse {
+ optional Payload payload = 1;
+}
+
+message StreamingInputCallRequest {
+ // Optional input payload sent along with the request.
+ optional Payload payload = 1;
+
+ // Not expecting any payload from the response.
+}
+
+message StreamingInputCallResponse {
+ // Aggregated size of payloads received from the client.
+ optional int32 aggregated_payload_size = 1;
+}
+
+message ResponseParameters {
+ // Desired payload sizes in responses from the server.
+ // If response_type is COMPRESSABLE, this denotes the size before compression.
+ required int32 size = 1;
+
+ // Desired interval between consecutive responses in the response stream in
+ // microseconds.
+ required int32 interval_us = 2;
+}
+
+message StreamingOutputCallRequest {
+ // Desired payload type in the response from the server.
+ // If response_type is RANDOM, the payload from each response in the stream
+ // might be of different types. This is to simulate a mixed type of payload
+ // stream.
+ optional PayloadType response_type = 1 [default=COMPRESSABLE];
+
+ repeated ResponseParameters response_parameters = 2;
+
+ // Optional input payload sent along with the request.
+ optional Payload payload = 3;
+}
+
+message StreamingOutputCallResponse {
+ optional Payload payload = 1;
+}
+
+service TestService {
+ // One request followed by one response.
+ // The server returns the client payload as-is.
+ rpc UnaryCall(SimpleRequest) returns (SimpleResponse);
+
+ // One request followed by a sequence of responses (streamed download).
+ // The server returns the payload with client desired type and sizes.
+ rpc StreamingOutputCall(StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+
+ // A sequence of requests followed by one response (streamed upload).
+ // The server returns the aggregated size of client payload as the result.
+ rpc StreamingInputCall(stream StreamingInputCallRequest)
+ returns (StreamingInputCallResponse);
+
+ // A sequence of requests with each request served by the server immediately.
+ // As one request could lead to multiple responses, this interface
+ // demonstrates the idea of full duplexing.
+ rpc FullDuplexCall(stream StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+
+ // A sequence of requests followed by a sequence of responses.
+ // The server buffers all the client requests and then serves them in order. A
+ // stream of responses are returned to the client when the server starts with
+ // first request.
+ rpc HalfDuplexCall(stream StreamingOutputCallRequest)
+ returns (stream StreamingOutputCallResponse);
+}
diff --git a/src/python/grpcio/tests/unit/__init__.py b/src/python/grpcio/tests/unit/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/_adapter/.gitignore b/src/python/grpcio/tests/unit/_adapter/.gitignore
new file mode 100644
index 0000000000..a6f96cd6db
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_adapter/.gitignore
@@ -0,0 +1,5 @@
+*.a
+*.so
+*.dll
+*.pyc
+*.pyd
diff --git a/src/python/grpcio/tests/unit/_adapter/__init__.py b/src/python/grpcio/tests/unit/_adapter/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_adapter/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/_adapter/_intermediary_low_test.py b/src/python/grpcio/tests/unit/_adapter/_intermediary_low_test.py
new file mode 100644
index 0000000000..a6fd82388c
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_adapter/_intermediary_low_test.py
@@ -0,0 +1,426 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests for the old '_low'."""
+
+import Queue
+import threading
+import time
+import unittest
+
+from grpc._adapter import _intermediary_low as _low
+
+_STREAM_LENGTH = 300
+_TIMEOUT = 5
+_AFTER_DELAY = 2
+_FUTURE = time.time() + 60 * 60 * 24
+_BYTE_SEQUENCE = b'\abcdefghijklmnopqrstuvwxyz0123456789' * 200
+_BYTE_SEQUENCE_SEQUENCE = tuple(
+ bytes(bytearray((row + column) % 256 for column in range(row)))
+ for row in range(_STREAM_LENGTH))
+
+
+class LonelyClientTest(unittest.TestCase):
+
+ def testLonelyClient(self):
+ host = 'nosuchhostexists'
+ port = 54321
+ method = 'test method'
+ deadline = time.time() + _TIMEOUT
+ after_deadline = deadline + _AFTER_DELAY
+ metadata_tag = object()
+ finish_tag = object()
+
+ completion_queue = _low.CompletionQueue()
+ channel = _low.Channel('%s:%d' % (host, port), None)
+ client_call = _low.Call(channel, completion_queue, method, host, deadline)
+
+ client_call.invoke(completion_queue, metadata_tag, finish_tag)
+ first_event = completion_queue.get(after_deadline)
+ self.assertIsNotNone(first_event)
+ second_event = completion_queue.get(after_deadline)
+ self.assertIsNotNone(second_event)
+ kinds = [event.kind for event in (first_event, second_event)]
+ self.assertItemsEqual(
+ (_low.Event.Kind.METADATA_ACCEPTED, _low.Event.Kind.FINISH),
+ kinds)
+
+ self.assertIsNone(completion_queue.get(after_deadline))
+
+ completion_queue.stop()
+ stop_event = completion_queue.get(_FUTURE)
+ self.assertEqual(_low.Event.Kind.STOP, stop_event.kind)
+
+ del client_call
+ del channel
+ del completion_queue
+
+
+def _drive_completion_queue(completion_queue, event_queue):
+ while True:
+ event = completion_queue.get(_FUTURE)
+ if event.kind is _low.Event.Kind.STOP:
+ break
+ event_queue.put(event)
+
+
+class EchoTest(unittest.TestCase):
+
+ def setUp(self):
+ self.host = 'localhost'
+
+ self.server_completion_queue = _low.CompletionQueue()
+ self.server = _low.Server(self.server_completion_queue)
+ port = self.server.add_http2_addr('[::]:0')
+ self.server.start()
+ self.server_events = Queue.Queue()
+ self.server_completion_queue_thread = threading.Thread(
+ target=_drive_completion_queue,
+ args=(self.server_completion_queue, self.server_events))
+ self.server_completion_queue_thread.start()
+
+ self.client_completion_queue = _low.CompletionQueue()
+ self.channel = _low.Channel('%s:%d' % (self.host, port), None)
+ self.client_events = Queue.Queue()
+ self.client_completion_queue_thread = threading.Thread(
+ target=_drive_completion_queue,
+ args=(self.client_completion_queue, self.client_events))
+ self.client_completion_queue_thread.start()
+
+ def tearDown(self):
+ self.server.stop()
+ self.server.cancel_all_calls()
+ self.server_completion_queue.stop()
+ self.client_completion_queue.stop()
+ self.server_completion_queue_thread.join()
+ self.client_completion_queue_thread.join()
+ del self.server
+
+ def _perform_echo_test(self, test_data):
+ method = 'test method'
+ details = 'test details'
+ server_leading_metadata_key = 'my_server_leading_key'
+ server_leading_metadata_value = 'my_server_leading_value'
+ server_trailing_metadata_key = 'my_server_trailing_key'
+ server_trailing_metadata_value = 'my_server_trailing_value'
+ client_metadata_key = 'my_client_key'
+ client_metadata_value = 'my_client_value'
+ server_leading_binary_metadata_key = 'my_server_leading_key-bin'
+ server_leading_binary_metadata_value = b'\0'*2047
+ server_trailing_binary_metadata_key = 'my_server_trailing_key-bin'
+ server_trailing_binary_metadata_value = b'\0'*2047
+ client_binary_metadata_key = 'my_client_key-bin'
+ client_binary_metadata_value = b'\0'*2047
+ deadline = _FUTURE
+ metadata_tag = object()
+ finish_tag = object()
+ write_tag = object()
+ complete_tag = object()
+ service_tag = object()
+ read_tag = object()
+ status_tag = object()
+
+ server_data = []
+ client_data = []
+
+ client_call = _low.Call(self.channel, self.client_completion_queue,
+ method, self.host, deadline)
+ client_call.add_metadata(client_metadata_key, client_metadata_value)
+ client_call.add_metadata(client_binary_metadata_key,
+ client_binary_metadata_value)
+
+ client_call.invoke(self.client_completion_queue, metadata_tag, finish_tag)
+
+ self.server.service(service_tag)
+ service_accepted = self.server_events.get()
+ self.assertIsNotNone(service_accepted)
+ self.assertIs(service_accepted.kind, _low.Event.Kind.SERVICE_ACCEPTED)
+ self.assertIs(service_accepted.tag, service_tag)
+ self.assertEqual(method, service_accepted.service_acceptance.method)
+ self.assertEqual(self.host, service_accepted.service_acceptance.host)
+ self.assertIsNotNone(service_accepted.service_acceptance.call)
+ metadata = dict(service_accepted.metadata)
+ self.assertIn(client_metadata_key, metadata)
+ self.assertEqual(client_metadata_value, metadata[client_metadata_key])
+ self.assertIn(client_binary_metadata_key, metadata)
+ self.assertEqual(client_binary_metadata_value,
+ metadata[client_binary_metadata_key])
+ server_call = service_accepted.service_acceptance.call
+ server_call.accept(self.server_completion_queue, finish_tag)
+ server_call.add_metadata(server_leading_metadata_key,
+ server_leading_metadata_value)
+ server_call.add_metadata(server_leading_binary_metadata_key,
+ server_leading_binary_metadata_value)
+ server_call.premetadata()
+
+ metadata_accepted = self.client_events.get()
+ self.assertIsNotNone(metadata_accepted)
+ self.assertEqual(_low.Event.Kind.METADATA_ACCEPTED, metadata_accepted.kind)
+ self.assertEqual(metadata_tag, metadata_accepted.tag)
+ metadata = dict(metadata_accepted.metadata)
+ self.assertIn(server_leading_metadata_key, metadata)
+ self.assertEqual(server_leading_metadata_value,
+ metadata[server_leading_metadata_key])
+ self.assertIn(server_leading_binary_metadata_key, metadata)
+ self.assertEqual(server_leading_binary_metadata_value,
+ metadata[server_leading_binary_metadata_key])
+
+ for datum in test_data:
+ client_call.write(datum, write_tag, _low.WriteFlags.WRITE_NO_COMPRESS)
+ write_accepted = self.client_events.get()
+ self.assertIsNotNone(write_accepted)
+ self.assertIs(write_accepted.kind, _low.Event.Kind.WRITE_ACCEPTED)
+ self.assertIs(write_accepted.tag, write_tag)
+ self.assertIs(write_accepted.write_accepted, True)
+
+ server_call.read(read_tag)
+ read_accepted = self.server_events.get()
+ self.assertIsNotNone(read_accepted)
+ self.assertEqual(_low.Event.Kind.READ_ACCEPTED, read_accepted.kind)
+ self.assertEqual(read_tag, read_accepted.tag)
+ self.assertIsNotNone(read_accepted.bytes)
+ server_data.append(read_accepted.bytes)
+
+ server_call.write(read_accepted.bytes, write_tag, 0)
+ write_accepted = self.server_events.get()
+ self.assertIsNotNone(write_accepted)
+ self.assertEqual(_low.Event.Kind.WRITE_ACCEPTED, write_accepted.kind)
+ self.assertEqual(write_tag, write_accepted.tag)
+ self.assertTrue(write_accepted.write_accepted)
+
+ client_call.read(read_tag)
+ read_accepted = self.client_events.get()
+ self.assertIsNotNone(read_accepted)
+ self.assertEqual(_low.Event.Kind.READ_ACCEPTED, read_accepted.kind)
+ self.assertEqual(read_tag, read_accepted.tag)
+ self.assertIsNotNone(read_accepted.bytes)
+ client_data.append(read_accepted.bytes)
+
+ client_call.complete(complete_tag)
+ complete_accepted = self.client_events.get()
+ self.assertIsNotNone(complete_accepted)
+ self.assertIs(complete_accepted.kind, _low.Event.Kind.COMPLETE_ACCEPTED)
+ self.assertIs(complete_accepted.tag, complete_tag)
+ self.assertIs(complete_accepted.complete_accepted, True)
+
+ server_call.read(read_tag)
+ read_accepted = self.server_events.get()
+ self.assertIsNotNone(read_accepted)
+ self.assertEqual(_low.Event.Kind.READ_ACCEPTED, read_accepted.kind)
+ self.assertEqual(read_tag, read_accepted.tag)
+ self.assertIsNone(read_accepted.bytes)
+
+ server_call.add_metadata(server_trailing_metadata_key,
+ server_trailing_metadata_value)
+ server_call.add_metadata(server_trailing_binary_metadata_key,
+ server_trailing_binary_metadata_value)
+
+ server_call.status(_low.Status(_low.Code.OK, details), status_tag)
+ server_terminal_event_one = self.server_events.get()
+ server_terminal_event_two = self.server_events.get()
+ if server_terminal_event_one.kind == _low.Event.Kind.COMPLETE_ACCEPTED:
+ status_accepted = server_terminal_event_one
+ rpc_accepted = server_terminal_event_two
+ else:
+ status_accepted = server_terminal_event_two
+ rpc_accepted = server_terminal_event_one
+ self.assertIsNotNone(status_accepted)
+ self.assertIsNotNone(rpc_accepted)
+ self.assertEqual(_low.Event.Kind.COMPLETE_ACCEPTED, status_accepted.kind)
+ self.assertEqual(status_tag, status_accepted.tag)
+ self.assertTrue(status_accepted.complete_accepted)
+ self.assertEqual(_low.Event.Kind.FINISH, rpc_accepted.kind)
+ self.assertEqual(finish_tag, rpc_accepted.tag)
+ self.assertEqual(_low.Status(_low.Code.OK, ''), rpc_accepted.status)
+
+ client_call.read(read_tag)
+ client_terminal_event_one = self.client_events.get()
+ client_terminal_event_two = self.client_events.get()
+ if client_terminal_event_one.kind == _low.Event.Kind.READ_ACCEPTED:
+ read_accepted = client_terminal_event_one
+ finish_accepted = client_terminal_event_two
+ else:
+ read_accepted = client_terminal_event_two
+ finish_accepted = client_terminal_event_one
+ self.assertIsNotNone(read_accepted)
+ self.assertIsNotNone(finish_accepted)
+ self.assertEqual(_low.Event.Kind.READ_ACCEPTED, read_accepted.kind)
+ self.assertEqual(read_tag, read_accepted.tag)
+ self.assertIsNone(read_accepted.bytes)
+ self.assertEqual(_low.Event.Kind.FINISH, finish_accepted.kind)
+ self.assertEqual(finish_tag, finish_accepted.tag)
+ self.assertEqual(_low.Status(_low.Code.OK, details), finish_accepted.status)
+ metadata = dict(finish_accepted.metadata)
+ self.assertIn(server_trailing_metadata_key, metadata)
+ self.assertEqual(server_trailing_metadata_value,
+ metadata[server_trailing_metadata_key])
+ self.assertIn(server_trailing_binary_metadata_key, metadata)
+ self.assertEqual(server_trailing_binary_metadata_value,
+ metadata[server_trailing_binary_metadata_key])
+ self.assertSetEqual(set(key for key, _ in finish_accepted.metadata),
+ set((server_trailing_metadata_key,
+ server_trailing_binary_metadata_key,)))
+
+ self.assertSequenceEqual(test_data, server_data)
+ self.assertSequenceEqual(test_data, client_data)
+
+ def testNoEcho(self):
+ self._perform_echo_test(())
+
+ def testOneByteEcho(self):
+ self._perform_echo_test([b'\x07'])
+
+ def testOneManyByteEcho(self):
+ self._perform_echo_test([_BYTE_SEQUENCE])
+
+ def testManyOneByteEchoes(self):
+ self._perform_echo_test(_BYTE_SEQUENCE)
+
+ def testManyManyByteEchoes(self):
+ self._perform_echo_test(_BYTE_SEQUENCE_SEQUENCE)
+
+
+class CancellationTest(unittest.TestCase):
+
+ def setUp(self):
+ self.host = 'localhost'
+
+ self.server_completion_queue = _low.CompletionQueue()
+ self.server = _low.Server(self.server_completion_queue)
+ port = self.server.add_http2_addr('[::]:0')
+ self.server.start()
+ self.server_events = Queue.Queue()
+ self.server_completion_queue_thread = threading.Thread(
+ target=_drive_completion_queue,
+ args=(self.server_completion_queue, self.server_events))
+ self.server_completion_queue_thread.start()
+
+ self.client_completion_queue = _low.CompletionQueue()
+ self.channel = _low.Channel('%s:%d' % (self.host, port), None)
+ self.client_events = Queue.Queue()
+ self.client_completion_queue_thread = threading.Thread(
+ target=_drive_completion_queue,
+ args=(self.client_completion_queue, self.client_events))
+ self.client_completion_queue_thread.start()
+
+ def tearDown(self):
+ self.server.stop()
+ self.server.cancel_all_calls()
+ self.server_completion_queue.stop()
+ self.client_completion_queue.stop()
+ self.server_completion_queue_thread.join()
+ self.client_completion_queue_thread.join()
+ del self.server
+
+ def testCancellation(self):
+ method = 'test method'
+ deadline = _FUTURE
+ metadata_tag = object()
+ finish_tag = object()
+ write_tag = object()
+ service_tag = object()
+ read_tag = object()
+ test_data = _BYTE_SEQUENCE_SEQUENCE
+
+ server_data = []
+ client_data = []
+
+ client_call = _low.Call(self.channel, self.client_completion_queue,
+ method, self.host, deadline)
+
+ client_call.invoke(self.client_completion_queue, metadata_tag, finish_tag)
+
+ self.server.service(service_tag)
+ service_accepted = self.server_events.get()
+ server_call = service_accepted.service_acceptance.call
+
+ server_call.accept(self.server_completion_queue, finish_tag)
+ server_call.premetadata()
+
+ metadata_accepted = self.client_events.get()
+ self.assertIsNotNone(metadata_accepted)
+
+ for datum in test_data:
+ client_call.write(datum, write_tag, 0)
+ write_accepted = self.client_events.get()
+
+ server_call.read(read_tag)
+ read_accepted = self.server_events.get()
+ server_data.append(read_accepted.bytes)
+
+ server_call.write(read_accepted.bytes, write_tag, 0)
+ write_accepted = self.server_events.get()
+ self.assertIsNotNone(write_accepted)
+
+ client_call.read(read_tag)
+ read_accepted = self.client_events.get()
+ client_data.append(read_accepted.bytes)
+
+ client_call.cancel()
+ # cancel() is idempotent.
+ client_call.cancel()
+ client_call.cancel()
+ client_call.cancel()
+
+ server_call.read(read_tag)
+
+ server_terminal_event_one = self.server_events.get()
+ server_terminal_event_two = self.server_events.get()
+ if server_terminal_event_one.kind == _low.Event.Kind.READ_ACCEPTED:
+ read_accepted = server_terminal_event_one
+ rpc_accepted = server_terminal_event_two
+ else:
+ read_accepted = server_terminal_event_two
+ rpc_accepted = server_terminal_event_one
+ self.assertIsNotNone(read_accepted)
+ self.assertIsNotNone(rpc_accepted)
+ self.assertEqual(_low.Event.Kind.READ_ACCEPTED, read_accepted.kind)
+ self.assertIsNone(read_accepted.bytes)
+ self.assertEqual(_low.Event.Kind.FINISH, rpc_accepted.kind)
+ self.assertEqual(_low.Status(_low.Code.CANCELLED, ''), rpc_accepted.status)
+
+ finish_event = self.client_events.get()
+ self.assertEqual(_low.Event.Kind.FINISH, finish_event.kind)
+ self.assertEqual(_low.Status(_low.Code.CANCELLED, 'Cancelled'),
+ finish_event.status)
+
+ self.assertSequenceEqual(test_data, server_data)
+ self.assertSequenceEqual(test_data, client_data)
+
+
+class ExpirationTest(unittest.TestCase):
+
+ @unittest.skip('TODO(nathaniel): Expiration test!')
+ def testExpiration(self):
+ pass
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
+
diff --git a/src/python/grpcio/tests/unit/_adapter/_low_test.py b/src/python/grpcio/tests/unit/_adapter/_low_test.py
new file mode 100644
index 0000000000..ec46617996
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_adapter/_low_test.py
@@ -0,0 +1,319 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import threading
+import time
+import unittest
+
+from grpc import _grpcio_metadata
+from grpc._adapter import _types
+from grpc._adapter import _low
+from tests.unit import test_common
+
+
+def wait_for_events(completion_queues, deadline):
+ """
+ Args:
+ completion_queues: list of completion queues to wait for events on
+ deadline: absolute deadline to wait until
+
+ Returns:
+ a sequence of events of length len(completion_queues).
+ """
+
+ results = [None] * len(completion_queues)
+ lock = threading.Lock()
+ threads = []
+ def set_ith_result(i, completion_queue):
+ result = completion_queue.next(deadline)
+ with lock:
+ results[i] = result
+ for i, completion_queue in enumerate(completion_queues):
+ thread = threading.Thread(target=set_ith_result,
+ args=[i, completion_queue])
+ thread.start()
+ threads.append(thread)
+ for thread in threads:
+ thread.join()
+ return results
+
+
+class InsecureServerInsecureClient(unittest.TestCase):
+
+ def setUp(self):
+ self.server_completion_queue = _low.CompletionQueue()
+ self.server = _low.Server(self.server_completion_queue, [])
+ self.port = self.server.add_http2_port('[::]:0')
+ self.client_completion_queue = _low.CompletionQueue()
+ self.client_channel = _low.Channel('localhost:%d'%self.port, [])
+
+ self.server.start()
+
+ def tearDown(self):
+ self.server.shutdown()
+ del self.client_channel
+
+ self.client_completion_queue.shutdown()
+ while (self.client_completion_queue.next(float('+inf')).type !=
+ _types.EventType.QUEUE_SHUTDOWN):
+ pass
+ self.server_completion_queue.shutdown()
+ while (self.server_completion_queue.next(float('+inf')).type !=
+ _types.EventType.QUEUE_SHUTDOWN):
+ pass
+
+ del self.client_completion_queue
+ del self.server_completion_queue
+ del self.server
+
+ def testEcho(self):
+ deadline = time.time() + 5
+ event_time_tolerance = 2
+ deadline_tolerance = 0.25
+ client_metadata_ascii_key = 'key'
+ client_metadata_ascii_value = 'val'
+ client_metadata_bin_key = 'key-bin'
+ client_metadata_bin_value = b'\0'*1000
+ server_initial_metadata_key = 'init_me_me_me'
+ server_initial_metadata_value = 'whodawha?'
+ server_trailing_metadata_key = 'california_is_in_a_drought'
+ server_trailing_metadata_value = 'zomg it is'
+ server_status_code = _types.StatusCode.OK
+ server_status_details = 'our work is never over'
+ request = 'blarghaflargh'
+ response = 'his name is robert paulson'
+ method = 'twinkies'
+ host = 'hostess'
+ server_request_tag = object()
+ request_call_result = self.server.request_call(self.server_completion_queue,
+ server_request_tag)
+
+ self.assertEqual(_types.CallError.OK, request_call_result)
+
+ client_call_tag = object()
+ client_call = self.client_channel.create_call(
+ self.client_completion_queue, method, host, deadline)
+ client_initial_metadata = [
+ (client_metadata_ascii_key, client_metadata_ascii_value),
+ (client_metadata_bin_key, client_metadata_bin_value)
+ ]
+ client_start_batch_result = client_call.start_batch([
+ _types.OpArgs.send_initial_metadata(client_initial_metadata),
+ _types.OpArgs.send_message(request, 0),
+ _types.OpArgs.send_close_from_client(),
+ _types.OpArgs.recv_initial_metadata(),
+ _types.OpArgs.recv_message(),
+ _types.OpArgs.recv_status_on_client()
+ ], client_call_tag)
+ self.assertEqual(_types.CallError.OK, client_start_batch_result)
+
+ client_no_event, request_event, = wait_for_events(
+ [self.client_completion_queue, self.server_completion_queue],
+ time.time() + event_time_tolerance)
+ self.assertEqual(client_no_event, None)
+ self.assertEqual(_types.EventType.OP_COMPLETE, request_event.type)
+ self.assertIsInstance(request_event.call, _low.Call)
+ self.assertIs(server_request_tag, request_event.tag)
+ self.assertEqual(1, len(request_event.results))
+ received_initial_metadata = request_event.results[0].initial_metadata
+ # Check that our metadata were transmitted
+ self.assertTrue(test_common.metadata_transmitted(client_initial_metadata,
+ received_initial_metadata))
+ # Check that Python's user agent string is a part of the full user agent
+ # string
+ received_initial_metadata_dict = dict(received_initial_metadata)
+ self.assertIn('user-agent', received_initial_metadata_dict)
+ self.assertIn('Python-gRPC-{}'.format(_grpcio_metadata.__version__),
+ received_initial_metadata_dict['user-agent'])
+ self.assertEqual(method, request_event.call_details.method)
+ self.assertEqual(host, request_event.call_details.host)
+ self.assertLess(abs(deadline - request_event.call_details.deadline),
+ deadline_tolerance)
+
+ # Check that the channel is connected, and that both it and the call have
+ # the proper target and peer; do this after the first flurry of messages to
+ # avoid the possibility that connection was delayed by the core until the
+ # first message was sent.
+ self.assertEqual(_types.ConnectivityState.READY,
+ self.client_channel.check_connectivity_state(False))
+ self.assertIsNotNone(self.client_channel.target())
+ self.assertIsNotNone(client_call.peer())
+
+ server_call_tag = object()
+ server_call = request_event.call
+ server_initial_metadata = [
+ (server_initial_metadata_key, server_initial_metadata_value)
+ ]
+ server_trailing_metadata = [
+ (server_trailing_metadata_key, server_trailing_metadata_value)
+ ]
+ server_start_batch_result = server_call.start_batch([
+ _types.OpArgs.send_initial_metadata(server_initial_metadata),
+ _types.OpArgs.recv_message(),
+ _types.OpArgs.send_message(response, 0),
+ _types.OpArgs.recv_close_on_server(),
+ _types.OpArgs.send_status_from_server(
+ server_trailing_metadata, server_status_code, server_status_details)
+ ], server_call_tag)
+ self.assertEqual(_types.CallError.OK, server_start_batch_result)
+
+ client_event, server_event, = wait_for_events(
+ [self.client_completion_queue, self.server_completion_queue],
+ time.time() + event_time_tolerance)
+
+ self.assertEqual(6, len(client_event.results))
+ found_client_op_types = set()
+ for client_result in client_event.results:
+ # we expect each op type to be unique
+ self.assertNotIn(client_result.type, found_client_op_types)
+ found_client_op_types.add(client_result.type)
+ if client_result.type == _types.OpType.RECV_INITIAL_METADATA:
+ self.assertTrue(
+ test_common.metadata_transmitted(server_initial_metadata,
+ client_result.initial_metadata))
+ elif client_result.type == _types.OpType.RECV_MESSAGE:
+ self.assertEqual(response, client_result.message)
+ elif client_result.type == _types.OpType.RECV_STATUS_ON_CLIENT:
+ self.assertTrue(
+ test_common.metadata_transmitted(server_trailing_metadata,
+ client_result.trailing_metadata))
+ self.assertEqual(server_status_details, client_result.status.details)
+ self.assertEqual(server_status_code, client_result.status.code)
+ self.assertEqual(set([
+ _types.OpType.SEND_INITIAL_METADATA,
+ _types.OpType.SEND_MESSAGE,
+ _types.OpType.SEND_CLOSE_FROM_CLIENT,
+ _types.OpType.RECV_INITIAL_METADATA,
+ _types.OpType.RECV_MESSAGE,
+ _types.OpType.RECV_STATUS_ON_CLIENT
+ ]), found_client_op_types)
+
+ self.assertEqual(5, len(server_event.results))
+ found_server_op_types = set()
+ for server_result in server_event.results:
+ self.assertNotIn(client_result.type, found_server_op_types)
+ found_server_op_types.add(server_result.type)
+ if server_result.type == _types.OpType.RECV_MESSAGE:
+ self.assertEqual(request, server_result.message)
+ elif server_result.type == _types.OpType.RECV_CLOSE_ON_SERVER:
+ self.assertFalse(server_result.cancelled)
+ self.assertEqual(set([
+ _types.OpType.SEND_INITIAL_METADATA,
+ _types.OpType.RECV_MESSAGE,
+ _types.OpType.SEND_MESSAGE,
+ _types.OpType.RECV_CLOSE_ON_SERVER,
+ _types.OpType.SEND_STATUS_FROM_SERVER
+ ]), found_server_op_types)
+
+ del client_call
+ del server_call
+
+
+class HangingServerShutdown(unittest.TestCase):
+
+ def setUp(self):
+ self.server_completion_queue = _low.CompletionQueue()
+ self.server = _low.Server(self.server_completion_queue, [])
+ self.port = self.server.add_http2_port('[::]:0')
+ self.client_completion_queue = _low.CompletionQueue()
+ self.client_channel = _low.Channel('localhost:%d'%self.port, [])
+
+ self.server.start()
+
+ def tearDown(self):
+ self.server.shutdown()
+ del self.client_channel
+
+ self.client_completion_queue.shutdown()
+ self.server_completion_queue.shutdown()
+ while True:
+ client_event, server_event = wait_for_events(
+ [self.client_completion_queue, self.server_completion_queue],
+ float("+inf"))
+ if (client_event.type == _types.EventType.QUEUE_SHUTDOWN and
+ server_event.type == _types.EventType.QUEUE_SHUTDOWN):
+ break
+
+ del self.client_completion_queue
+ del self.server_completion_queue
+ del self.server
+
+ def testHangingServerCall(self):
+ deadline = time.time() + 5
+ deadline_tolerance = 0.25
+ event_time_tolerance = 2
+ cancel_all_calls_time_tolerance = 0.5
+ request = 'blarghaflargh'
+ method = 'twinkies'
+ host = 'hostess'
+ server_request_tag = object()
+ request_call_result = self.server.request_call(self.server_completion_queue,
+ server_request_tag)
+
+ client_call_tag = object()
+ client_call = self.client_channel.create_call(self.client_completion_queue,
+ method, host, deadline)
+ client_start_batch_result = client_call.start_batch([
+ _types.OpArgs.send_initial_metadata([]),
+ _types.OpArgs.send_message(request, 0),
+ _types.OpArgs.send_close_from_client(),
+ _types.OpArgs.recv_initial_metadata(),
+ _types.OpArgs.recv_message(),
+ _types.OpArgs.recv_status_on_client()
+ ], client_call_tag)
+
+ client_no_event, request_event, = wait_for_events(
+ [self.client_completion_queue, self.server_completion_queue],
+ time.time() + event_time_tolerance)
+
+ # Now try to shutdown the server and expect that we see server shutdown
+ # almost immediately after calling cancel_all_calls.
+
+ # First attempt to cancel all calls before shutting down, and expect
+ # our state machine to catch the erroneous API use.
+ with self.assertRaises(RuntimeError):
+ self.server.cancel_all_calls()
+
+ shutdown_tag = object()
+ self.server.shutdown(shutdown_tag)
+ pre_cancel_timestamp = time.time()
+ self.server.cancel_all_calls()
+ finish_shutdown_timestamp = None
+ client_call_event, server_shutdown_event = wait_for_events(
+ [self.client_completion_queue, self.server_completion_queue],
+ time.time() + event_time_tolerance)
+ self.assertIs(shutdown_tag, server_shutdown_event.tag)
+ self.assertGreater(pre_cancel_timestamp + cancel_all_calls_time_tolerance,
+ time.time())
+
+ del client_call
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/_adapter/_proto_scenarios.py b/src/python/grpcio/tests/unit/_adapter/_proto_scenarios.py
new file mode 100644
index 0000000000..f55a7a23ea
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_adapter/_proto_scenarios.py
@@ -0,0 +1,261 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Test scenarios using protocol buffers."""
+
+import abc
+import threading
+
+from tests.unit._junkdrawer import math_pb2
+
+
+class ProtoScenario(object):
+ """An RPC test scenario using protocol buffers."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def method(self):
+ """Access the test method name.
+
+ Returns:
+ The test method name.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_request(self, request):
+ """Serialize a request protocol buffer.
+
+ Args:
+ request: A request protocol buffer.
+
+ Returns:
+ The bytestring serialization of the given request protocol buffer.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_request(self, request_bytestring):
+ """Deserialize a request protocol buffer.
+
+ Args:
+ request_bytestring: The bytestring serialization of a request protocol
+ buffer.
+
+ Returns:
+ The request protocol buffer deserialized from the given byte string.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_response(self, response):
+ """Serialize a response protocol buffer.
+
+ Args:
+ response: A response protocol buffer.
+
+ Returns:
+ The bytestring serialization of the given response protocol buffer.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_response(self, response_bytestring):
+ """Deserialize a response protocol buffer.
+
+ Args:
+ response_bytestring: The bytestring serialization of a response protocol
+ buffer.
+
+ Returns:
+ The response protocol buffer deserialized from the given byte string.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def requests(self):
+ """Access the sequence of requests for this scenario.
+
+ Returns:
+ A sequence of request protocol buffers.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def response_for_request(self, request):
+ """Access the response for a particular request.
+
+ Args:
+ request: A request protocol buffer.
+
+ Returns:
+ The response protocol buffer appropriate for the given request.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify_requests(self, experimental_requests):
+ """Verify the requests transmitted through the system under test.
+
+ Args:
+ experimental_requests: The request protocol buffers transmitted through
+ the system under test.
+
+ Returns:
+ True if the requests satisfy this test scenario; False otherwise.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify_responses(self, experimental_responses):
+ """Verify the responses transmitted through the system under test.
+
+ Args:
+ experimental_responses: The response protocol buffers transmitted through
+ the system under test.
+
+ Returns:
+ True if the responses satisfy this test scenario; False otherwise.
+ """
+ raise NotImplementedError()
+
+
+class EmptyScenario(ProtoScenario):
+ """A scenario that transmits no protocol buffers in either direction."""
+
+ def method(self):
+ return 'DivMany'
+
+ def serialize_request(self, request):
+ raise ValueError('This should not be necessary to call!')
+
+ def deserialize_request(self, request_bytestring):
+ raise ValueError('This should not be necessary to call!')
+
+ def serialize_response(self, response):
+ raise ValueError('This should not be necessary to call!')
+
+ def deserialize_response(self, response_bytestring):
+ raise ValueError('This should not be necessary to call!')
+
+ def requests(self):
+ return ()
+
+ def response_for_request(self, request):
+ raise ValueError('This should not be necessary to call!')
+
+ def verify_requests(self, experimental_requests):
+ return not experimental_requests
+
+ def verify_responses(self, experimental_responses):
+ return not experimental_responses
+
+
+class BidirectionallyUnaryScenario(ProtoScenario):
+ """A scenario that transmits no protocol buffers in either direction."""
+
+ _DIVIDEND = 59
+ _DIVISOR = 7
+ _QUOTIENT = 8
+ _REMAINDER = 3
+
+ _REQUEST = math_pb2.DivArgs(dividend=_DIVIDEND, divisor=_DIVISOR)
+ _RESPONSE = math_pb2.DivReply(quotient=_QUOTIENT, remainder=_REMAINDER)
+
+ def method(self):
+ return 'Div'
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, request_bytestring):
+ return math_pb2.DivArgs.FromString(request_bytestring)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, response_bytestring):
+ return math_pb2.DivReply.FromString(response_bytestring)
+
+ def requests(self):
+ return [self._REQUEST]
+
+ def response_for_request(self, request):
+ return self._RESPONSE
+
+ def verify_requests(self, experimental_requests):
+ return tuple(experimental_requests) == (self._REQUEST,)
+
+ def verify_responses(self, experimental_responses):
+ return tuple(experimental_responses) == (self._RESPONSE,)
+
+
+class BidirectionallyStreamingScenario(ProtoScenario):
+ """A scenario that transmits no protocol buffers in either direction."""
+
+ _STREAM_LENGTH = 200
+ _REQUESTS = tuple(
+ math_pb2.DivArgs(dividend=59 + index, divisor=7 + index)
+ for index in range(_STREAM_LENGTH))
+
+ def __init__(self):
+ self._lock = threading.Lock()
+ self._responses = []
+
+ def method(self):
+ return 'DivMany'
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, request_bytestring):
+ return math_pb2.DivArgs.FromString(request_bytestring)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, response_bytestring):
+ return math_pb2.DivReply.FromString(response_bytestring)
+
+ def requests(self):
+ return self._REQUESTS
+
+ def response_for_request(self, request):
+ quotient, remainder = divmod(request.dividend, request.divisor)
+ response = math_pb2.DivReply(quotient=quotient, remainder=remainder)
+ with self._lock:
+ self._responses.append(response)
+ return response
+
+ def verify_requests(self, experimental_requests):
+ return tuple(experimental_requests) == self._REQUESTS
+
+ def verify_responses(self, experimental_responses):
+ with self._lock:
+ return tuple(experimental_responses) == tuple(self._responses)
diff --git a/src/python/grpcio/tests/unit/_core_over_links_base_interface_test.py b/src/python/grpcio/tests/unit/_core_over_links_base_interface_test.py
new file mode 100644
index 0000000000..efc990421a
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_core_over_links_base_interface_test.py
@@ -0,0 +1,155 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests Base interface compliance of the core-over-gRPC-links stack."""
+
+import collections
+import logging
+import random
+import time
+import unittest
+
+from grpc._adapter import _intermediary_low
+from grpc._links import invocation
+from grpc._links import service
+from grpc.beta import interfaces as beta_interfaces
+from grpc.framework.core import implementations
+from grpc.framework.interfaces.base import utilities
+from tests.unit import test_common as grpc_test_common
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.base import test_cases
+from tests.unit.framework.interfaces.base import test_interfaces
+
+
+class _SerializationBehaviors(
+ collections.namedtuple(
+ '_SerializationBehaviors',
+ ('request_serializers', 'request_deserializers', 'response_serializers',
+ 'response_deserializers',))):
+ pass
+
+
+class _Links(
+ collections.namedtuple(
+ '_Links',
+ ('invocation_end_link', 'invocation_grpc_link', 'service_grpc_link',
+ 'service_end_link'))):
+ pass
+
+
+def _serialization_behaviors_from_serializations(serializations):
+ request_serializers = {}
+ request_deserializers = {}
+ response_serializers = {}
+ response_deserializers = {}
+ for (group, method), serialization in serializations.iteritems():
+ request_serializers[group, method] = serialization.serialize_request
+ request_deserializers[group, method] = serialization.deserialize_request
+ response_serializers[group, method] = serialization.serialize_response
+ response_deserializers[group, method] = serialization.deserialize_response
+ return _SerializationBehaviors(
+ request_serializers, request_deserializers, response_serializers,
+ response_deserializers)
+
+
+class _Implementation(test_interfaces.Implementation):
+
+ def instantiate(self, serializations, servicer):
+ serialization_behaviors = _serialization_behaviors_from_serializations(
+ serializations)
+ invocation_end_link = implementations.invocation_end_link()
+ service_end_link = implementations.service_end_link(
+ servicer, test_constants.DEFAULT_TIMEOUT,
+ test_constants.MAXIMUM_TIMEOUT)
+ service_grpc_link = service.service_link(
+ serialization_behaviors.request_deserializers,
+ serialization_behaviors.response_serializers)
+ port = service_grpc_link.add_port('[::]:0', None)
+ channel = _intermediary_low.Channel('localhost:%d' % port, None)
+ invocation_grpc_link = invocation.invocation_link(
+ channel, b'localhost', None,
+ serialization_behaviors.request_serializers,
+ serialization_behaviors.response_deserializers)
+
+ invocation_end_link.join_link(invocation_grpc_link)
+ invocation_grpc_link.join_link(invocation_end_link)
+ service_end_link.join_link(service_grpc_link)
+ service_grpc_link.join_link(service_end_link)
+ invocation_grpc_link.start()
+ service_grpc_link.start()
+ return invocation_end_link, service_end_link, (
+ invocation_grpc_link, service_grpc_link)
+
+ def destantiate(self, memo):
+ invocation_grpc_link, service_grpc_link = memo
+ invocation_grpc_link.stop()
+ service_grpc_link.begin_stop()
+ service_grpc_link.end_stop()
+
+ def invocation_initial_metadata(self):
+ return grpc_test_common.INVOCATION_INITIAL_METADATA
+
+ def service_initial_metadata(self):
+ return grpc_test_common.SERVICE_INITIAL_METADATA
+
+ def invocation_completion(self):
+ return utilities.completion(None, None, None)
+
+ def service_completion(self):
+ return utilities.completion(
+ grpc_test_common.SERVICE_TERMINAL_METADATA,
+ beta_interfaces.StatusCode.OK, grpc_test_common.DETAILS)
+
+ def metadata_transmitted(self, original_metadata, transmitted_metadata):
+ return original_metadata is None or grpc_test_common.metadata_transmitted(
+ original_metadata, transmitted_metadata)
+
+ def completion_transmitted(self, original_completion, transmitted_completion):
+ if (original_completion.terminal_metadata is not None and
+ not grpc_test_common.metadata_transmitted(
+ original_completion.terminal_metadata,
+ transmitted_completion.terminal_metadata)):
+ return False
+ elif original_completion.code is not transmitted_completion.code:
+ return False
+ elif original_completion.message != transmitted_completion.message:
+ return False
+ else:
+ return True
+
+
+def load_tests(loader, tests, pattern):
+ return unittest.TestSuite(
+ tests=tuple(
+ loader.loadTestsFromTestCase(test_case_class)
+ for test_case_class in test_cases.test_cases(_Implementation())))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/_crust_over_core_over_links_face_interface_test.py b/src/python/grpcio/tests/unit/_crust_over_core_over_links_face_interface_test.py
new file mode 100644
index 0000000000..4faaaadc2b
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_crust_over_core_over_links_face_interface_test.py
@@ -0,0 +1,161 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests Face compliance of the crust-over-core-over-gRPC-links stack."""
+
+import collections
+import unittest
+
+from grpc._adapter import _intermediary_low
+from grpc._links import invocation
+from grpc._links import service
+from grpc.beta import interfaces as beta_interfaces
+from grpc.framework.core import implementations as core_implementations
+from grpc.framework.crust import implementations as crust_implementations
+from grpc.framework.foundation import logging_pool
+from grpc.framework.interfaces.links import utilities
+from tests.unit import test_common as grpc_test_common
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.face import test_cases
+from tests.unit.framework.interfaces.face import test_interfaces
+
+
+class _SerializationBehaviors(
+ collections.namedtuple(
+ '_SerializationBehaviors',
+ ('request_serializers', 'request_deserializers', 'response_serializers',
+ 'response_deserializers',))):
+ pass
+
+
+def _serialization_behaviors_from_test_methods(test_methods):
+ request_serializers = {}
+ request_deserializers = {}
+ response_serializers = {}
+ response_deserializers = {}
+ for (group, method), test_method in test_methods.iteritems():
+ request_serializers[group, method] = test_method.serialize_request
+ request_deserializers[group, method] = test_method.deserialize_request
+ response_serializers[group, method] = test_method.serialize_response
+ response_deserializers[group, method] = test_method.deserialize_response
+ return _SerializationBehaviors(
+ request_serializers, request_deserializers, response_serializers,
+ response_deserializers)
+
+
+class _Implementation(test_interfaces.Implementation):
+
+ def instantiate(
+ self, methods, method_implementations, multi_method_implementation):
+ pool = logging_pool.pool(test_constants.POOL_SIZE)
+ servicer = crust_implementations.servicer(
+ method_implementations, multi_method_implementation, pool)
+ serialization_behaviors = _serialization_behaviors_from_test_methods(
+ methods)
+ invocation_end_link = core_implementations.invocation_end_link()
+ service_end_link = core_implementations.service_end_link(
+ servicer, test_constants.DEFAULT_TIMEOUT,
+ test_constants.MAXIMUM_TIMEOUT)
+ service_grpc_link = service.service_link(
+ serialization_behaviors.request_deserializers,
+ serialization_behaviors.response_serializers)
+ port = service_grpc_link.add_port('[::]:0', None)
+ channel = _intermediary_low.Channel('localhost:%d' % port, None)
+ invocation_grpc_link = invocation.invocation_link(
+ channel, b'localhost', None,
+ serialization_behaviors.request_serializers,
+ serialization_behaviors.response_deserializers)
+
+ invocation_end_link.join_link(invocation_grpc_link)
+ invocation_grpc_link.join_link(invocation_end_link)
+ service_grpc_link.join_link(service_end_link)
+ service_end_link.join_link(service_grpc_link)
+ service_end_link.start()
+ invocation_end_link.start()
+ invocation_grpc_link.start()
+ service_grpc_link.start()
+
+ generic_stub = crust_implementations.generic_stub(invocation_end_link, pool)
+ # TODO(nathaniel): Add a "groups" attribute to _digest.TestServiceDigest.
+ group = next(iter(methods))[0]
+ # TODO(nathaniel): Add a "cardinalities_by_group" attribute to
+ # _digest.TestServiceDigest.
+ cardinalities = {
+ method: method_object.cardinality()
+ for (group, method), method_object in methods.iteritems()}
+ dynamic_stub = crust_implementations.dynamic_stub(
+ invocation_end_link, group, cardinalities, pool)
+
+ return generic_stub, {group: dynamic_stub}, (
+ invocation_end_link, invocation_grpc_link, service_grpc_link,
+ service_end_link, pool)
+
+ def destantiate(self, memo):
+ (invocation_end_link, invocation_grpc_link, service_grpc_link,
+ service_end_link, pool) = memo
+ invocation_end_link.stop(0).wait()
+ invocation_grpc_link.stop()
+ service_grpc_link.begin_stop()
+ service_end_link.stop(0).wait()
+ service_grpc_link.end_stop()
+ invocation_end_link.join_link(utilities.NULL_LINK)
+ invocation_grpc_link.join_link(utilities.NULL_LINK)
+ service_grpc_link.join_link(utilities.NULL_LINK)
+ service_end_link.join_link(utilities.NULL_LINK)
+ pool.shutdown(wait=True)
+
+ def invocation_metadata(self):
+ return grpc_test_common.INVOCATION_INITIAL_METADATA
+
+ def initial_metadata(self):
+ return grpc_test_common.SERVICE_INITIAL_METADATA
+
+ def terminal_metadata(self):
+ return grpc_test_common.SERVICE_TERMINAL_METADATA
+
+ def code(self):
+ return beta_interfaces.StatusCode.OK
+
+ def details(self):
+ return grpc_test_common.DETAILS
+
+ def metadata_transmitted(self, original_metadata, transmitted_metadata):
+ return original_metadata is None or grpc_test_common.metadata_transmitted(
+ original_metadata, transmitted_metadata)
+
+
+def load_tests(loader, tests, pattern):
+ return unittest.TestSuite(
+ tests=tuple(
+ loader.loadTestsFromTestCase(test_case_class)
+ for test_case_class in test_cases.test_cases(_Implementation())))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/_cython/.gitignore b/src/python/grpcio/tests/unit/_cython/.gitignore
new file mode 100644
index 0000000000..c315029288
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_cython/.gitignore
@@ -0,0 +1,7 @@
+*.h
+*.c
+*.a
+*.so
+*.dll
+*.pyc
+*.pyd
diff --git a/src/python/grpcio/tests/unit/_cython/__init__.py b/src/python/grpcio/tests/unit/_cython/__init__.py
new file mode 100644
index 0000000000..b89398809f
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_cython/__init__.py
@@ -0,0 +1,28 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/python/grpcio/tests/unit/_cython/cygrpc_test.py b/src/python/grpcio/tests/unit/_cython/cygrpc_test.py
new file mode 100644
index 0000000000..876da88de9
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_cython/cygrpc_test.py
@@ -0,0 +1,452 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import time
+import threading
+import unittest
+
+from grpc._cython import cygrpc
+from tests.unit._cython import test_utilities
+from tests.unit import test_common
+from tests.unit import resources
+
+
+_SSL_HOST_OVERRIDE = 'foo.test.google.fr'
+_CALL_CREDENTIALS_METADATA_KEY = 'call-creds-key'
+_CALL_CREDENTIALS_METADATA_VALUE = 'call-creds-value'
+
+def _metadata_plugin_callback(context, callback):
+ callback(cygrpc.Metadata(
+ [cygrpc.Metadatum(_CALL_CREDENTIALS_METADATA_KEY,
+ _CALL_CREDENTIALS_METADATA_VALUE)]),
+ cygrpc.StatusCode.ok, '')
+
+
+class TypeSmokeTest(unittest.TestCase):
+
+ def testStringsInUtilitiesUpDown(self):
+ self.assertEqual(0, cygrpc.StatusCode.ok)
+ metadatum = cygrpc.Metadatum('a', 'b')
+ self.assertEqual('a'.encode(), metadatum.key)
+ self.assertEqual('b'.encode(), metadatum.value)
+ metadata = cygrpc.Metadata([metadatum])
+ self.assertEqual(1, len(metadata))
+ self.assertEqual(metadatum.key, metadata[0].key)
+
+ def testMetadataIteration(self):
+ metadata = cygrpc.Metadata([
+ cygrpc.Metadatum('a', 'b'), cygrpc.Metadatum('c', 'd')])
+ iterator = iter(metadata)
+ metadatum = next(iterator)
+ self.assertIsInstance(metadatum, cygrpc.Metadatum)
+ self.assertEqual(metadatum.key, 'a'.encode())
+ self.assertEqual(metadatum.value, 'b'.encode())
+ metadatum = next(iterator)
+ self.assertIsInstance(metadatum, cygrpc.Metadatum)
+ self.assertEqual(metadatum.key, 'c'.encode())
+ self.assertEqual(metadatum.value, 'd'.encode())
+ with self.assertRaises(StopIteration):
+ next(iterator)
+
+ def testOperationsIteration(self):
+ operations = cygrpc.Operations([
+ cygrpc.operation_send_message('asdf')])
+ iterator = iter(operations)
+ operation = next(iterator)
+ self.assertIsInstance(operation, cygrpc.Operation)
+ # `Operation`s are write-only structures; can't directly debug anything out
+ # of them. Just check that we stop iterating.
+ with self.assertRaises(StopIteration):
+ next(iterator)
+
+ def testTimespec(self):
+ now = time.time()
+ timespec = cygrpc.Timespec(now)
+ self.assertAlmostEqual(now, float(timespec), places=8)
+
+ def testCompletionQueueUpDown(self):
+ completion_queue = cygrpc.CompletionQueue()
+ del completion_queue
+
+ def testServerUpDown(self):
+ server = cygrpc.Server(cygrpc.ChannelArgs([]))
+ del server
+
+ def testChannelUpDown(self):
+ channel = cygrpc.Channel('[::]:0', cygrpc.ChannelArgs([]))
+ del channel
+
+ def testCredentialsMetadataPluginUpDown(self):
+ plugin = cygrpc.CredentialsMetadataPlugin(
+ lambda ignored_a, ignored_b: None, '')
+ del plugin
+
+ def testCallCredentialsFromPluginUpDown(self):
+ plugin = cygrpc.CredentialsMetadataPlugin(_metadata_plugin_callback, '')
+ call_credentials = cygrpc.call_credentials_metadata_plugin(plugin)
+ del plugin
+ del call_credentials
+
+ def testServerStartNoExplicitShutdown(self):
+ server = cygrpc.Server()
+ completion_queue = cygrpc.CompletionQueue()
+ server.register_completion_queue(completion_queue)
+ port = server.add_http2_port('[::]:0')
+ self.assertIsInstance(port, int)
+ server.start()
+ del server
+
+ def testServerStartShutdown(self):
+ completion_queue = cygrpc.CompletionQueue()
+ server = cygrpc.Server()
+ server.add_http2_port('[::]:0')
+ server.register_completion_queue(completion_queue)
+ server.start()
+ shutdown_tag = object()
+ server.shutdown(completion_queue, shutdown_tag)
+ event = completion_queue.poll()
+ self.assertEqual(cygrpc.CompletionType.operation_complete, event.type)
+ self.assertIs(shutdown_tag, event.tag)
+ del server
+ del completion_queue
+
+
+class InsecureServerInsecureClient(unittest.TestCase):
+
+ def setUp(self):
+ self.server_completion_queue = cygrpc.CompletionQueue()
+ self.server = cygrpc.Server()
+ self.server.register_completion_queue(self.server_completion_queue)
+ self.port = self.server.add_http2_port('[::]:0')
+ self.server.start()
+ self.client_completion_queue = cygrpc.CompletionQueue()
+ self.client_channel = cygrpc.Channel('localhost:{}'.format(self.port))
+
+ def tearDown(self):
+ del self.server
+ del self.client_completion_queue
+ del self.server_completion_queue
+
+ def testEcho(self):
+ DEADLINE = time.time()+5
+ DEADLINE_TOLERANCE = 0.25
+ CLIENT_METADATA_ASCII_KEY = b'key'
+ CLIENT_METADATA_ASCII_VALUE = b'val'
+ CLIENT_METADATA_BIN_KEY = b'key-bin'
+ CLIENT_METADATA_BIN_VALUE = b'\0'*1000
+ SERVER_INITIAL_METADATA_KEY = b'init_me_me_me'
+ SERVER_INITIAL_METADATA_VALUE = b'whodawha?'
+ SERVER_TRAILING_METADATA_KEY = b'california_is_in_a_drought'
+ SERVER_TRAILING_METADATA_VALUE = b'zomg it is'
+ SERVER_STATUS_CODE = cygrpc.StatusCode.ok
+ SERVER_STATUS_DETAILS = b'our work is never over'
+ REQUEST = b'in death a member of project mayhem has a name'
+ RESPONSE = b'his name is robert paulson'
+ METHOD = b'twinkies'
+ HOST = b'hostess'
+
+ cygrpc_deadline = cygrpc.Timespec(DEADLINE)
+
+ server_request_tag = object()
+ request_call_result = self.server.request_call(
+ self.server_completion_queue, self.server_completion_queue,
+ server_request_tag)
+
+ self.assertEqual(cygrpc.CallError.ok, request_call_result)
+
+ client_call_tag = object()
+ client_call = self.client_channel.create_call(
+ None, 0, self.client_completion_queue, METHOD, HOST, cygrpc_deadline)
+ client_initial_metadata = cygrpc.Metadata([
+ cygrpc.Metadatum(CLIENT_METADATA_ASCII_KEY,
+ CLIENT_METADATA_ASCII_VALUE),
+ cygrpc.Metadatum(CLIENT_METADATA_BIN_KEY, CLIENT_METADATA_BIN_VALUE)])
+ client_start_batch_result = client_call.start_batch(cygrpc.Operations([
+ cygrpc.operation_send_initial_metadata(client_initial_metadata),
+ cygrpc.operation_send_message(REQUEST),
+ cygrpc.operation_send_close_from_client(),
+ cygrpc.operation_receive_initial_metadata(),
+ cygrpc.operation_receive_message(),
+ cygrpc.operation_receive_status_on_client()
+ ]), client_call_tag)
+ self.assertEqual(cygrpc.CallError.ok, client_start_batch_result)
+ client_event_future = test_utilities.CompletionQueuePollFuture(
+ self.client_completion_queue, cygrpc_deadline)
+
+ request_event = self.server_completion_queue.poll(cygrpc_deadline)
+ self.assertEqual(cygrpc.CompletionType.operation_complete,
+ request_event.type)
+ self.assertIsInstance(request_event.operation_call, cygrpc.Call)
+ self.assertIs(server_request_tag, request_event.tag)
+ self.assertEqual(0, len(request_event.batch_operations))
+ self.assertTrue(
+ test_common.metadata_transmitted(client_initial_metadata,
+ request_event.request_metadata))
+ self.assertEqual(METHOD, request_event.request_call_details.method)
+ self.assertEqual(HOST, request_event.request_call_details.host)
+ self.assertLess(
+ abs(DEADLINE - float(request_event.request_call_details.deadline)),
+ DEADLINE_TOLERANCE)
+
+ server_call_tag = object()
+ server_call = request_event.operation_call
+ server_initial_metadata = cygrpc.Metadata([
+ cygrpc.Metadatum(SERVER_INITIAL_METADATA_KEY,
+ SERVER_INITIAL_METADATA_VALUE)])
+ server_trailing_metadata = cygrpc.Metadata([
+ cygrpc.Metadatum(SERVER_TRAILING_METADATA_KEY,
+ SERVER_TRAILING_METADATA_VALUE)])
+ server_start_batch_result = server_call.start_batch([
+ cygrpc.operation_send_initial_metadata(server_initial_metadata),
+ cygrpc.operation_receive_message(),
+ cygrpc.operation_send_message(RESPONSE),
+ cygrpc.operation_receive_close_on_server(),
+ cygrpc.operation_send_status_from_server(
+ server_trailing_metadata, SERVER_STATUS_CODE, SERVER_STATUS_DETAILS)
+ ], server_call_tag)
+ self.assertEqual(cygrpc.CallError.ok, server_start_batch_result)
+
+ client_event = client_event_future.result()
+ server_event = self.server_completion_queue.poll(cygrpc_deadline)
+
+ self.assertEqual(6, len(client_event.batch_operations))
+ found_client_op_types = set()
+ for client_result in client_event.batch_operations:
+ # we expect each op type to be unique
+ self.assertNotIn(client_result.type, found_client_op_types)
+ found_client_op_types.add(client_result.type)
+ if client_result.type == cygrpc.OperationType.receive_initial_metadata:
+ self.assertTrue(
+ test_common.metadata_transmitted(server_initial_metadata,
+ client_result.received_metadata))
+ elif client_result.type == cygrpc.OperationType.receive_message:
+ self.assertEqual(RESPONSE, client_result.received_message.bytes())
+ elif client_result.type == cygrpc.OperationType.receive_status_on_client:
+ self.assertTrue(
+ test_common.metadata_transmitted(server_trailing_metadata,
+ client_result.received_metadata))
+ self.assertEqual(SERVER_STATUS_DETAILS,
+ client_result.received_status_details)
+ self.assertEqual(SERVER_STATUS_CODE, client_result.received_status_code)
+ self.assertEqual(set([
+ cygrpc.OperationType.send_initial_metadata,
+ cygrpc.OperationType.send_message,
+ cygrpc.OperationType.send_close_from_client,
+ cygrpc.OperationType.receive_initial_metadata,
+ cygrpc.OperationType.receive_message,
+ cygrpc.OperationType.receive_status_on_client
+ ]), found_client_op_types)
+
+ self.assertEqual(5, len(server_event.batch_operations))
+ found_server_op_types = set()
+ for server_result in server_event.batch_operations:
+ self.assertNotIn(client_result.type, found_server_op_types)
+ found_server_op_types.add(server_result.type)
+ if server_result.type == cygrpc.OperationType.receive_message:
+ self.assertEqual(REQUEST, server_result.received_message.bytes())
+ elif server_result.type == cygrpc.OperationType.receive_close_on_server:
+ self.assertFalse(server_result.received_cancelled)
+ self.assertEqual(set([
+ cygrpc.OperationType.send_initial_metadata,
+ cygrpc.OperationType.receive_message,
+ cygrpc.OperationType.send_message,
+ cygrpc.OperationType.receive_close_on_server,
+ cygrpc.OperationType.send_status_from_server
+ ]), found_server_op_types)
+
+ del client_call
+ del server_call
+
+
+class SecureServerSecureClient(unittest.TestCase):
+
+ def setUp(self):
+ server_credentials = cygrpc.server_credentials_ssl(
+ None, [cygrpc.SslPemKeyCertPair(resources.private_key(),
+ resources.certificate_chain())], False)
+ channel_credentials = cygrpc.channel_credentials_ssl(
+ resources.test_root_certificates(), None)
+ self.server_completion_queue = cygrpc.CompletionQueue()
+ self.server = cygrpc.Server()
+ self.server.register_completion_queue(self.server_completion_queue)
+ self.port = self.server.add_http2_port('[::]:0', server_credentials)
+ self.server.start()
+ self.client_completion_queue = cygrpc.CompletionQueue()
+ client_channel_arguments = cygrpc.ChannelArgs([
+ cygrpc.ChannelArg(cygrpc.ChannelArgKey.ssl_target_name_override,
+ _SSL_HOST_OVERRIDE)])
+ self.client_channel = cygrpc.Channel(
+ 'localhost:{}'.format(self.port), client_channel_arguments,
+ channel_credentials)
+
+ def tearDown(self):
+ del self.server
+ del self.client_completion_queue
+ del self.server_completion_queue
+
+ def testEcho(self):
+ DEADLINE = time.time()+5
+ DEADLINE_TOLERANCE = 0.25
+ CLIENT_METADATA_ASCII_KEY = b'key'
+ CLIENT_METADATA_ASCII_VALUE = b'val'
+ CLIENT_METADATA_BIN_KEY = b'key-bin'
+ CLIENT_METADATA_BIN_VALUE = b'\0'*1000
+ SERVER_INITIAL_METADATA_KEY = b'init_me_me_me'
+ SERVER_INITIAL_METADATA_VALUE = b'whodawha?'
+ SERVER_TRAILING_METADATA_KEY = b'california_is_in_a_drought'
+ SERVER_TRAILING_METADATA_VALUE = b'zomg it is'
+ SERVER_STATUS_CODE = cygrpc.StatusCode.ok
+ SERVER_STATUS_DETAILS = b'our work is never over'
+ REQUEST = b'in death a member of project mayhem has a name'
+ RESPONSE = b'his name is robert paulson'
+ METHOD = b'/twinkies'
+ HOST = None # Default host
+
+ cygrpc_deadline = cygrpc.Timespec(DEADLINE)
+
+ server_request_tag = object()
+ request_call_result = self.server.request_call(
+ self.server_completion_queue, self.server_completion_queue,
+ server_request_tag)
+
+ self.assertEqual(cygrpc.CallError.ok, request_call_result)
+
+ plugin = cygrpc.CredentialsMetadataPlugin(_metadata_plugin_callback, '')
+ call_credentials = cygrpc.call_credentials_metadata_plugin(plugin)
+
+ client_call_tag = object()
+ client_call = self.client_channel.create_call(
+ None, 0, self.client_completion_queue, METHOD, HOST, cygrpc_deadline)
+ client_call.set_credentials(call_credentials)
+ client_initial_metadata = cygrpc.Metadata([
+ cygrpc.Metadatum(CLIENT_METADATA_ASCII_KEY,
+ CLIENT_METADATA_ASCII_VALUE),
+ cygrpc.Metadatum(CLIENT_METADATA_BIN_KEY, CLIENT_METADATA_BIN_VALUE)])
+ client_start_batch_result = client_call.start_batch(cygrpc.Operations([
+ cygrpc.operation_send_initial_metadata(client_initial_metadata),
+ cygrpc.operation_send_message(REQUEST),
+ cygrpc.operation_send_close_from_client(),
+ cygrpc.operation_receive_initial_metadata(),
+ cygrpc.operation_receive_message(),
+ cygrpc.operation_receive_status_on_client()
+ ]), client_call_tag)
+ self.assertEqual(cygrpc.CallError.ok, client_start_batch_result)
+ client_event_future = test_utilities.CompletionQueuePollFuture(
+ self.client_completion_queue, cygrpc_deadline)
+
+ request_event = self.server_completion_queue.poll(cygrpc_deadline)
+ self.assertEqual(cygrpc.CompletionType.operation_complete,
+ request_event.type)
+ self.assertIsInstance(request_event.operation_call, cygrpc.Call)
+ self.assertIs(server_request_tag, request_event.tag)
+ self.assertEqual(0, len(request_event.batch_operations))
+ client_metadata_with_credentials = list(client_initial_metadata) + [
+ (_CALL_CREDENTIALS_METADATA_KEY, _CALL_CREDENTIALS_METADATA_VALUE)]
+ self.assertTrue(
+ test_common.metadata_transmitted(client_metadata_with_credentials,
+ request_event.request_metadata))
+ self.assertEqual(METHOD, request_event.request_call_details.method)
+ self.assertEqual(_SSL_HOST_OVERRIDE,
+ request_event.request_call_details.host)
+ self.assertLess(
+ abs(DEADLINE - float(request_event.request_call_details.deadline)),
+ DEADLINE_TOLERANCE)
+
+ server_call_tag = object()
+ server_call = request_event.operation_call
+ server_initial_metadata = cygrpc.Metadata([
+ cygrpc.Metadatum(SERVER_INITIAL_METADATA_KEY,
+ SERVER_INITIAL_METADATA_VALUE)])
+ server_trailing_metadata = cygrpc.Metadata([
+ cygrpc.Metadatum(SERVER_TRAILING_METADATA_KEY,
+ SERVER_TRAILING_METADATA_VALUE)])
+ server_start_batch_result = server_call.start_batch([
+ cygrpc.operation_send_initial_metadata(server_initial_metadata),
+ cygrpc.operation_receive_message(),
+ cygrpc.operation_send_message(RESPONSE),
+ cygrpc.operation_receive_close_on_server(),
+ cygrpc.operation_send_status_from_server(
+ server_trailing_metadata, SERVER_STATUS_CODE, SERVER_STATUS_DETAILS)
+ ], server_call_tag)
+ self.assertEqual(cygrpc.CallError.ok, server_start_batch_result)
+
+ client_event = client_event_future.result()
+ server_event = self.server_completion_queue.poll(cygrpc_deadline)
+
+ self.assertEqual(6, len(client_event.batch_operations))
+ found_client_op_types = set()
+ for client_result in client_event.batch_operations:
+ # we expect each op type to be unique
+ self.assertNotIn(client_result.type, found_client_op_types)
+ found_client_op_types.add(client_result.type)
+ if client_result.type == cygrpc.OperationType.receive_initial_metadata:
+ self.assertTrue(
+ test_common.metadata_transmitted(server_initial_metadata,
+ client_result.received_metadata))
+ elif client_result.type == cygrpc.OperationType.receive_message:
+ self.assertEqual(RESPONSE, client_result.received_message.bytes())
+ elif client_result.type == cygrpc.OperationType.receive_status_on_client:
+ self.assertTrue(
+ test_common.metadata_transmitted(server_trailing_metadata,
+ client_result.received_metadata))
+ self.assertEqual(SERVER_STATUS_DETAILS,
+ client_result.received_status_details)
+ self.assertEqual(SERVER_STATUS_CODE, client_result.received_status_code)
+ self.assertEqual(set([
+ cygrpc.OperationType.send_initial_metadata,
+ cygrpc.OperationType.send_message,
+ cygrpc.OperationType.send_close_from_client,
+ cygrpc.OperationType.receive_initial_metadata,
+ cygrpc.OperationType.receive_message,
+ cygrpc.OperationType.receive_status_on_client
+ ]), found_client_op_types)
+
+ self.assertEqual(5, len(server_event.batch_operations))
+ found_server_op_types = set()
+ for server_result in server_event.batch_operations:
+ self.assertNotIn(client_result.type, found_server_op_types)
+ found_server_op_types.add(server_result.type)
+ if server_result.type == cygrpc.OperationType.receive_message:
+ self.assertEqual(REQUEST, server_result.received_message.bytes())
+ elif server_result.type == cygrpc.OperationType.receive_close_on_server:
+ self.assertFalse(server_result.received_cancelled)
+ self.assertEqual(set([
+ cygrpc.OperationType.send_initial_metadata,
+ cygrpc.OperationType.receive_message,
+ cygrpc.OperationType.send_message,
+ cygrpc.OperationType.receive_close_on_server,
+ cygrpc.OperationType.send_status_from_server
+ ]), found_server_op_types)
+
+ del client_call
+ del server_call
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/_cython/test_utilities.py b/src/python/grpcio/tests/unit/_cython/test_utilities.py
new file mode 100644
index 0000000000..21ea3075b4
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_cython/test_utilities.py
@@ -0,0 +1,46 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import threading
+
+from grpc._cython._cygrpc import completion_queue
+
+
+class CompletionQueuePollFuture:
+
+ def __init__(self, completion_queue, deadline):
+ def poller_function():
+ self._event_result = completion_queue.poll(deadline)
+ self._event_result = None
+ self._thread = threading.Thread(target=poller_function)
+ self._thread.start()
+
+ def result(self):
+ self._thread.join()
+ return self._event_result
diff --git a/src/python/grpcio/tests/unit/_junkdrawer/__init__.py b/src/python/grpcio/tests/unit/_junkdrawer/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_junkdrawer/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/_junkdrawer/math_pb2.py b/src/python/grpcio/tests/unit/_junkdrawer/math_pb2.py
new file mode 100644
index 0000000000..20165955b4
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_junkdrawer/math_pb2.py
@@ -0,0 +1,266 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# TODO(nathaniel): Remove this from source control after having made
+# generation from the math.proto source part of GRPC's build-and-test
+# process.
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: math.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='math.proto',
+ package='math',
+ serialized_pb=_b('\n\nmath.proto\x12\x04math\",\n\x07\x44ivArgs\x12\x10\n\x08\x64ividend\x18\x01 \x02(\x03\x12\x0f\n\x07\x64ivisor\x18\x02 \x02(\x03\"/\n\x08\x44ivReply\x12\x10\n\x08quotient\x18\x01 \x02(\x03\x12\x11\n\tremainder\x18\x02 \x02(\x03\"\x18\n\x07\x46ibArgs\x12\r\n\x05limit\x18\x01 \x01(\x03\"\x12\n\x03Num\x12\x0b\n\x03num\x18\x01 \x02(\x03\"\x19\n\x08\x46ibReply\x12\r\n\x05\x63ount\x18\x01 \x02(\x03\x32\xa4\x01\n\x04Math\x12&\n\x03\x44iv\x12\r.math.DivArgs\x1a\x0e.math.DivReply\"\x00\x12.\n\x07\x44ivMany\x12\r.math.DivArgs\x1a\x0e.math.DivReply\"\x00(\x01\x30\x01\x12#\n\x03\x46ib\x12\r.math.FibArgs\x1a\t.math.Num\"\x00\x30\x01\x12\x1f\n\x03Sum\x12\t.math.Num\x1a\t.math.Num\"\x00(\x01')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_DIVARGS = _descriptor.Descriptor(
+ name='DivArgs',
+ full_name='math.DivArgs',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='dividend', full_name='math.DivArgs.dividend', index=0,
+ number=1, type=3, cpp_type=2, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='divisor', full_name='math.DivArgs.divisor', index=1,
+ number=2, type=3, cpp_type=2, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=20,
+ serialized_end=64,
+)
+
+
+_DIVREPLY = _descriptor.Descriptor(
+ name='DivReply',
+ full_name='math.DivReply',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='quotient', full_name='math.DivReply.quotient', index=0,
+ number=1, type=3, cpp_type=2, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='remainder', full_name='math.DivReply.remainder', index=1,
+ number=2, type=3, cpp_type=2, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=66,
+ serialized_end=113,
+)
+
+
+_FIBARGS = _descriptor.Descriptor(
+ name='FibArgs',
+ full_name='math.FibArgs',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='limit', full_name='math.FibArgs.limit', index=0,
+ number=1, type=3, cpp_type=2, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=115,
+ serialized_end=139,
+)
+
+
+_NUM = _descriptor.Descriptor(
+ name='Num',
+ full_name='math.Num',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='num', full_name='math.Num.num', index=0,
+ number=1, type=3, cpp_type=2, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=141,
+ serialized_end=159,
+)
+
+
+_FIBREPLY = _descriptor.Descriptor(
+ name='FibReply',
+ full_name='math.FibReply',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='count', full_name='math.FibReply.count', index=0,
+ number=1, type=3, cpp_type=2, label=2,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=161,
+ serialized_end=186,
+)
+
+DESCRIPTOR.message_types_by_name['DivArgs'] = _DIVARGS
+DESCRIPTOR.message_types_by_name['DivReply'] = _DIVREPLY
+DESCRIPTOR.message_types_by_name['FibArgs'] = _FIBARGS
+DESCRIPTOR.message_types_by_name['Num'] = _NUM
+DESCRIPTOR.message_types_by_name['FibReply'] = _FIBREPLY
+
+DivArgs = _reflection.GeneratedProtocolMessageType('DivArgs', (_message.Message,), dict(
+ DESCRIPTOR = _DIVARGS,
+ __module__ = 'math_pb2'
+ # @@protoc_insertion_point(class_scope:math.DivArgs)
+ ))
+_sym_db.RegisterMessage(DivArgs)
+
+DivReply = _reflection.GeneratedProtocolMessageType('DivReply', (_message.Message,), dict(
+ DESCRIPTOR = _DIVREPLY,
+ __module__ = 'math_pb2'
+ # @@protoc_insertion_point(class_scope:math.DivReply)
+ ))
+_sym_db.RegisterMessage(DivReply)
+
+FibArgs = _reflection.GeneratedProtocolMessageType('FibArgs', (_message.Message,), dict(
+ DESCRIPTOR = _FIBARGS,
+ __module__ = 'math_pb2'
+ # @@protoc_insertion_point(class_scope:math.FibArgs)
+ ))
+_sym_db.RegisterMessage(FibArgs)
+
+Num = _reflection.GeneratedProtocolMessageType('Num', (_message.Message,), dict(
+ DESCRIPTOR = _NUM,
+ __module__ = 'math_pb2'
+ # @@protoc_insertion_point(class_scope:math.Num)
+ ))
+_sym_db.RegisterMessage(Num)
+
+FibReply = _reflection.GeneratedProtocolMessageType('FibReply', (_message.Message,), dict(
+ DESCRIPTOR = _FIBREPLY,
+ __module__ = 'math_pb2'
+ # @@protoc_insertion_point(class_scope:math.FibReply)
+ ))
+_sym_db.RegisterMessage(FibReply)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/src/python/grpcio/tests/unit/_junkdrawer/stock_pb2.py b/src/python/grpcio/tests/unit/_junkdrawer/stock_pb2.py
new file mode 100644
index 0000000000..eef18f82d6
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_junkdrawer/stock_pb2.py
@@ -0,0 +1,152 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# TODO(nathaniel): Remove this from source control after having made
+# generation from the stock.proto source part of GRPC's build-and-test
+# process.
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: stock.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+ name='stock.proto',
+ package='stock',
+ serialized_pb=_b('\n\x0bstock.proto\x12\x05stock\">\n\x0cStockRequest\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\x1e\n\x13num_trades_to_watch\x18\x02 \x01(\x05:\x01\x30\"+\n\nStockReply\x12\r\n\x05price\x18\x01 \x01(\x02\x12\x0e\n\x06symbol\x18\x02 \x01(\t2\x96\x02\n\x05Stock\x12=\n\x11GetLastTradePrice\x12\x13.stock.StockRequest\x1a\x11.stock.StockReply\"\x00\x12I\n\x19GetLastTradePriceMultiple\x12\x13.stock.StockRequest\x1a\x11.stock.StockReply\"\x00(\x01\x30\x01\x12?\n\x11WatchFutureTrades\x12\x13.stock.StockRequest\x1a\x11.stock.StockReply\"\x00\x30\x01\x12\x42\n\x14GetHighestTradePrice\x12\x13.stock.StockRequest\x1a\x11.stock.StockReply\"\x00(\x01')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_STOCKREQUEST = _descriptor.Descriptor(
+ name='StockRequest',
+ full_name='stock.StockRequest',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='symbol', full_name='stock.StockRequest.symbol', index=0,
+ number=1, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='num_trades_to_watch', full_name='stock.StockRequest.num_trades_to_watch', index=1,
+ number=2, type=5, cpp_type=1, label=1,
+ has_default_value=True, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=22,
+ serialized_end=84,
+)
+
+
+_STOCKREPLY = _descriptor.Descriptor(
+ name='StockReply',
+ full_name='stock.StockReply',
+ filename=None,
+ file=DESCRIPTOR,
+ containing_type=None,
+ fields=[
+ _descriptor.FieldDescriptor(
+ name='price', full_name='stock.StockReply.price', index=0,
+ number=1, type=2, cpp_type=6, label=1,
+ has_default_value=False, default_value=0,
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ _descriptor.FieldDescriptor(
+ name='symbol', full_name='stock.StockReply.symbol', index=1,
+ number=2, type=9, cpp_type=9, label=1,
+ has_default_value=False, default_value=_b("").decode('utf-8'),
+ message_type=None, enum_type=None, containing_type=None,
+ is_extension=False, extension_scope=None,
+ options=None),
+ ],
+ extensions=[
+ ],
+ nested_types=[],
+ enum_types=[
+ ],
+ options=None,
+ is_extendable=False,
+ extension_ranges=[],
+ oneofs=[
+ ],
+ serialized_start=86,
+ serialized_end=129,
+)
+
+DESCRIPTOR.message_types_by_name['StockRequest'] = _STOCKREQUEST
+DESCRIPTOR.message_types_by_name['StockReply'] = _STOCKREPLY
+
+StockRequest = _reflection.GeneratedProtocolMessageType('StockRequest', (_message.Message,), dict(
+ DESCRIPTOR = _STOCKREQUEST,
+ __module__ = 'stock_pb2'
+ # @@protoc_insertion_point(class_scope:stock.StockRequest)
+ ))
+_sym_db.RegisterMessage(StockRequest)
+
+StockReply = _reflection.GeneratedProtocolMessageType('StockReply', (_message.Message,), dict(
+ DESCRIPTOR = _STOCKREPLY,
+ __module__ = 'stock_pb2'
+ # @@protoc_insertion_point(class_scope:stock.StockReply)
+ ))
+_sym_db.RegisterMessage(StockReply)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/src/python/grpcio/tests/unit/_links/__init__.py b/src/python/grpcio/tests/unit/_links/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_links/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/_links/_lonely_invocation_link_test.py b/src/python/grpcio/tests/unit/_links/_lonely_invocation_link_test.py
new file mode 100644
index 0000000000..890755f81c
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_links/_lonely_invocation_link_test.py
@@ -0,0 +1,88 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A test of invocation-side code unconnected to an RPC server."""
+
+import unittest
+
+from grpc._adapter import _intermediary_low
+from grpc._links import invocation
+from grpc.framework.interfaces.links import links
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.links import test_cases
+from tests.unit.framework.interfaces.links import test_utilities
+
+_NULL_BEHAVIOR = lambda unused_argument: None
+
+
+class LonelyInvocationLinkTest(unittest.TestCase):
+
+ def testUpAndDown(self):
+ channel = _intermediary_low.Channel('nonexistent:54321', None)
+ invocation_link = invocation.invocation_link(
+ channel, 'nonexistent', None, {}, {})
+
+ invocation_link.start()
+ invocation_link.stop()
+
+ def _test_lonely_invocation_with_termination(self, termination):
+ test_operation_id = object()
+ test_group = 'test package.Test Service'
+ test_method = 'test method'
+ invocation_link_mate = test_utilities.RecordingLink()
+
+ channel = _intermediary_low.Channel('nonexistent:54321', None)
+ invocation_link = invocation.invocation_link(
+ channel, 'nonexistent', None, {}, {})
+ invocation_link.join_link(invocation_link_mate)
+ invocation_link.start()
+
+ ticket = links.Ticket(
+ test_operation_id, 0, test_group, test_method,
+ links.Ticket.Subscription.FULL, test_constants.SHORT_TIMEOUT, 1, None,
+ None, None, None, None, termination, None)
+ invocation_link.accept_ticket(ticket)
+ invocation_link_mate.block_until_tickets_satisfy(test_cases.terminated)
+
+ invocation_link.stop()
+
+ self.assertIsNot(
+ invocation_link_mate.tickets()[-1].termination,
+ links.Ticket.Termination.COMPLETION)
+
+ def testLonelyInvocationLinkWithCommencementTicket(self):
+ self._test_lonely_invocation_with_termination(None)
+
+ def testLonelyInvocationLinkWithEntireTicket(self):
+ self._test_lonely_invocation_with_termination(
+ links.Ticket.Termination.COMPLETION)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/src/python/grpcio/tests/unit/_links/_proto_scenarios.py b/src/python/grpcio/tests/unit/_links/_proto_scenarios.py
new file mode 100644
index 0000000000..f69ff51b16
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_links/_proto_scenarios.py
@@ -0,0 +1,261 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Test scenarios using protocol buffers."""
+
+import abc
+import threading
+
+from tests.unit._junkdrawer import math_pb2
+from tests.unit.framework.common import test_constants
+
+
+class ProtoScenario(object):
+ """An RPC test scenario using protocol buffers."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def group_and_method(self):
+ """Access the test group and method.
+
+ Returns:
+ The test group and method as a pair.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_request(self, request):
+ """Serialize a request protocol buffer.
+
+ Args:
+ request: A request protocol buffer.
+
+ Returns:
+ The bytestring serialization of the given request protocol buffer.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_request(self, request_bytestring):
+ """Deserialize a request protocol buffer.
+
+ Args:
+ request_bytestring: The bytestring serialization of a request protocol
+ buffer.
+
+ Returns:
+ The request protocol buffer deserialized from the given byte string.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_response(self, response):
+ """Serialize a response protocol buffer.
+
+ Args:
+ response: A response protocol buffer.
+
+ Returns:
+ The bytestring serialization of the given response protocol buffer.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_response(self, response_bytestring):
+ """Deserialize a response protocol buffer.
+
+ Args:
+ response_bytestring: The bytestring serialization of a response protocol
+ buffer.
+
+ Returns:
+ The response protocol buffer deserialized from the given byte string.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def requests(self):
+ """Access the sequence of requests for this scenario.
+
+ Returns:
+ A sequence of request protocol buffers.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def response_for_request(self, request):
+ """Access the response for a particular request.
+
+ Args:
+ request: A request protocol buffer.
+
+ Returns:
+ The response protocol buffer appropriate for the given request.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify_requests(self, experimental_requests):
+ """Verify the requests transmitted through the system under test.
+
+ Args:
+ experimental_requests: The request protocol buffers transmitted through
+ the system under test.
+
+ Returns:
+ True if the requests satisfy this test scenario; False otherwise.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify_responses(self, experimental_responses):
+ """Verify the responses transmitted through the system under test.
+
+ Args:
+ experimental_responses: The response protocol buffers transmitted through
+ the system under test.
+
+ Returns:
+ True if the responses satisfy this test scenario; False otherwise.
+ """
+ raise NotImplementedError()
+
+
+class EmptyScenario(ProtoScenario):
+ """A scenario that transmits no protocol buffers in either direction."""
+
+ def group_and_method(self):
+ return 'math.Math', 'DivMany'
+
+ def serialize_request(self, request):
+ raise ValueError('This should not be necessary to call!')
+
+ def deserialize_request(self, request_bytestring):
+ raise ValueError('This should not be necessary to call!')
+
+ def serialize_response(self, response):
+ raise ValueError('This should not be necessary to call!')
+
+ def deserialize_response(self, response_bytestring):
+ raise ValueError('This should not be necessary to call!')
+
+ def requests(self):
+ return ()
+
+ def response_for_request(self, request):
+ raise ValueError('This should not be necessary to call!')
+
+ def verify_requests(self, experimental_requests):
+ return not experimental_requests
+
+ def verify_responses(self, experimental_responses):
+ return not experimental_responses
+
+
+class BidirectionallyUnaryScenario(ProtoScenario):
+ """A scenario that transmits no protocol buffers in either direction."""
+
+ _DIVIDEND = 59
+ _DIVISOR = 7
+ _QUOTIENT = 8
+ _REMAINDER = 3
+
+ _REQUEST = math_pb2.DivArgs(dividend=_DIVIDEND, divisor=_DIVISOR)
+ _RESPONSE = math_pb2.DivReply(quotient=_QUOTIENT, remainder=_REMAINDER)
+
+ def group_and_method(self):
+ return 'math.Math', 'Div'
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, request_bytestring):
+ return math_pb2.DivArgs.FromString(request_bytestring)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, response_bytestring):
+ return math_pb2.DivReply.FromString(response_bytestring)
+
+ def requests(self):
+ return [self._REQUEST]
+
+ def response_for_request(self, request):
+ return self._RESPONSE
+
+ def verify_requests(self, experimental_requests):
+ return tuple(experimental_requests) == (self._REQUEST,)
+
+ def verify_responses(self, experimental_responses):
+ return tuple(experimental_responses) == (self._RESPONSE,)
+
+
+class BidirectionallyStreamingScenario(ProtoScenario):
+ """A scenario that transmits no protocol buffers in either direction."""
+
+ _REQUESTS = tuple(
+ math_pb2.DivArgs(dividend=59 + index, divisor=7 + index)
+ for index in range(test_constants.STREAM_LENGTH))
+
+ def __init__(self):
+ self._lock = threading.Lock()
+ self._responses = []
+
+ def group_and_method(self):
+ return 'math.Math', 'DivMany'
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, request_bytestring):
+ return math_pb2.DivArgs.FromString(request_bytestring)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, response_bytestring):
+ return math_pb2.DivReply.FromString(response_bytestring)
+
+ def requests(self):
+ return self._REQUESTS
+
+ def response_for_request(self, request):
+ quotient, remainder = divmod(request.dividend, request.divisor)
+ response = math_pb2.DivReply(quotient=quotient, remainder=remainder)
+ with self._lock:
+ self._responses.append(response)
+ return response
+
+ def verify_requests(self, experimental_requests):
+ return tuple(experimental_requests) == self._REQUESTS
+
+ def verify_responses(self, experimental_responses):
+ with self._lock:
+ return tuple(experimental_responses) == tuple(self._responses)
diff --git a/src/python/grpcio/tests/unit/_links/_transmission_test.py b/src/python/grpcio/tests/unit/_links/_transmission_test.py
new file mode 100644
index 0000000000..888684d197
--- /dev/null
+++ b/src/python/grpcio/tests/unit/_links/_transmission_test.py
@@ -0,0 +1,239 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests transmission of tickets across gRPC-on-the-wire."""
+
+import unittest
+
+from grpc._adapter import _intermediary_low
+from grpc._links import invocation
+from grpc._links import service
+from grpc.beta import interfaces as beta_interfaces
+from grpc.framework.interfaces.links import links
+from tests.unit import test_common
+from tests.unit._links import _proto_scenarios
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.links import test_cases
+from tests.unit.framework.interfaces.links import test_utilities
+
+_IDENTITY = lambda x: x
+
+
+class TransmissionTest(test_cases.TransmissionTest, unittest.TestCase):
+
+ def create_transmitting_links(self):
+ service_link = service.service_link(
+ {self.group_and_method(): self.deserialize_request},
+ {self.group_and_method(): self.serialize_response})
+ port = service_link.add_port('[::]:0', None)
+ service_link.start()
+ channel = _intermediary_low.Channel('localhost:%d' % port, None)
+ invocation_link = invocation.invocation_link(
+ channel, 'localhost', None,
+ {self.group_and_method(): self.serialize_request},
+ {self.group_and_method(): self.deserialize_response})
+ invocation_link.start()
+ return invocation_link, service_link
+
+ def destroy_transmitting_links(self, invocation_side_link, service_side_link):
+ invocation_side_link.stop()
+ service_side_link.begin_stop()
+ service_side_link.end_stop()
+
+ def create_invocation_initial_metadata(self):
+ return (
+ ('first_invocation_initial_metadata_key', 'just a string value'),
+ ('second_invocation_initial_metadata_key', '0123456789'),
+ ('third_invocation_initial_metadata_key-bin', '\x00\x57' * 100),
+ )
+
+ def create_invocation_terminal_metadata(self):
+ return None
+
+ def create_service_initial_metadata(self):
+ return (
+ ('first_service_initial_metadata_key', 'just another string value'),
+ ('second_service_initial_metadata_key', '9876543210'),
+ ('third_service_initial_metadata_key-bin', '\x00\x59\x02' * 100),
+ )
+
+ def create_service_terminal_metadata(self):
+ return (
+ ('first_service_terminal_metadata_key', 'yet another string value'),
+ ('second_service_terminal_metadata_key', 'abcdefghij'),
+ ('third_service_terminal_metadata_key-bin', '\x00\x37' * 100),
+ )
+
+ def create_invocation_completion(self):
+ return None, None
+
+ def create_service_completion(self):
+ return (
+ beta_interfaces.StatusCode.OK, b'An exuberant test "details" message!')
+
+ def assertMetadataTransmitted(self, original_metadata, transmitted_metadata):
+ self.assertTrue(
+ test_common.metadata_transmitted(
+ original_metadata, transmitted_metadata),
+ '%s erroneously transmitted as %s' % (
+ original_metadata, transmitted_metadata))
+
+
+class RoundTripTest(unittest.TestCase):
+
+ def testZeroMessageRoundTrip(self):
+ test_operation_id = object()
+ test_group = 'test package.Test Group'
+ test_method = 'test method'
+ identity_transformation = {(test_group, test_method): _IDENTITY}
+ test_code = beta_interfaces.StatusCode.OK
+ test_message = 'a test message'
+
+ service_link = service.service_link(
+ identity_transformation, identity_transformation)
+ service_mate = test_utilities.RecordingLink()
+ service_link.join_link(service_mate)
+ port = service_link.add_port('[::]:0', None)
+ service_link.start()
+ channel = _intermediary_low.Channel('localhost:%d' % port, None)
+ invocation_link = invocation.invocation_link(
+ channel, None, None, identity_transformation, identity_transformation)
+ invocation_mate = test_utilities.RecordingLink()
+ invocation_link.join_link(invocation_mate)
+ invocation_link.start()
+
+ invocation_ticket = links.Ticket(
+ test_operation_id, 0, test_group, test_method,
+ links.Ticket.Subscription.FULL, test_constants.LONG_TIMEOUT, None, None,
+ None, None, None, None, links.Ticket.Termination.COMPLETION, None)
+ invocation_link.accept_ticket(invocation_ticket)
+ service_mate.block_until_tickets_satisfy(test_cases.terminated)
+
+ service_ticket = links.Ticket(
+ service_mate.tickets()[-1].operation_id, 0, None, None, None, None,
+ None, None, None, None, test_code, test_message,
+ links.Ticket.Termination.COMPLETION, None)
+ service_link.accept_ticket(service_ticket)
+ invocation_mate.block_until_tickets_satisfy(test_cases.terminated)
+
+ invocation_link.stop()
+ service_link.begin_stop()
+ service_link.end_stop()
+
+ self.assertIs(
+ service_mate.tickets()[-1].termination,
+ links.Ticket.Termination.COMPLETION)
+ self.assertIs(
+ invocation_mate.tickets()[-1].termination,
+ links.Ticket.Termination.COMPLETION)
+ self.assertIs(invocation_mate.tickets()[-1].code, test_code)
+ self.assertEqual(invocation_mate.tickets()[-1].message, test_message)
+
+ def _perform_scenario_test(self, scenario):
+ test_operation_id = object()
+ test_group, test_method = scenario.group_and_method()
+ test_code = beta_interfaces.StatusCode.OK
+ test_message = 'a scenario test message'
+
+ service_link = service.service_link(
+ {(test_group, test_method): scenario.deserialize_request},
+ {(test_group, test_method): scenario.serialize_response})
+ service_mate = test_utilities.RecordingLink()
+ service_link.join_link(service_mate)
+ port = service_link.add_port('[::]:0', None)
+ service_link.start()
+ channel = _intermediary_low.Channel('localhost:%d' % port, None)
+ invocation_link = invocation.invocation_link(
+ channel, 'localhost', None,
+ {(test_group, test_method): scenario.serialize_request},
+ {(test_group, test_method): scenario.deserialize_response})
+ invocation_mate = test_utilities.RecordingLink()
+ invocation_link.join_link(invocation_mate)
+ invocation_link.start()
+
+ invocation_ticket = links.Ticket(
+ test_operation_id, 0, test_group, test_method,
+ links.Ticket.Subscription.FULL, test_constants.LONG_TIMEOUT, None, None,
+ None, None, None, None, None, None)
+ invocation_link.accept_ticket(invocation_ticket)
+ requests = scenario.requests()
+ for request_index, request in enumerate(requests):
+ request_ticket = links.Ticket(
+ test_operation_id, 1 + request_index, None, None, None, None, 1, None,
+ request, None, None, None, None, None)
+ invocation_link.accept_ticket(request_ticket)
+ service_mate.block_until_tickets_satisfy(
+ test_cases.at_least_n_payloads_received_predicate(1 + request_index))
+ response_ticket = links.Ticket(
+ service_mate.tickets()[0].operation_id, request_index, None, None,
+ None, None, 1, None, scenario.response_for_request(request), None,
+ None, None, None, None)
+ service_link.accept_ticket(response_ticket)
+ invocation_mate.block_until_tickets_satisfy(
+ test_cases.at_least_n_payloads_received_predicate(1 + request_index))
+ request_count = len(requests)
+ invocation_completion_ticket = links.Ticket(
+ test_operation_id, request_count + 1, None, None, None, None, None,
+ None, None, None, None, None, links.Ticket.Termination.COMPLETION,
+ None)
+ invocation_link.accept_ticket(invocation_completion_ticket)
+ service_mate.block_until_tickets_satisfy(test_cases.terminated)
+ service_completion_ticket = links.Ticket(
+ service_mate.tickets()[0].operation_id, request_count, None, None, None,
+ None, None, None, None, None, test_code, test_message,
+ links.Ticket.Termination.COMPLETION, None)
+ service_link.accept_ticket(service_completion_ticket)
+ invocation_mate.block_until_tickets_satisfy(test_cases.terminated)
+
+ invocation_link.stop()
+ service_link.begin_stop()
+ service_link.end_stop()
+
+ observed_requests = tuple(
+ ticket.payload for ticket in service_mate.tickets()
+ if ticket.payload is not None)
+ observed_responses = tuple(
+ ticket.payload for ticket in invocation_mate.tickets()
+ if ticket.payload is not None)
+ self.assertTrue(scenario.verify_requests(observed_requests))
+ self.assertTrue(scenario.verify_responses(observed_responses))
+
+ def testEmptyScenario(self):
+ self._perform_scenario_test(_proto_scenarios.EmptyScenario())
+
+ def testBidirectionallyUnaryScenario(self):
+ self._perform_scenario_test(_proto_scenarios.BidirectionallyUnaryScenario())
+
+ def testBidirectionallyStreamingScenario(self):
+ self._perform_scenario_test(
+ _proto_scenarios.BidirectionallyStreamingScenario())
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/beta/__init__.py b/src/python/grpcio/tests/unit/beta/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/beta/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/beta/_beta_features_test.py b/src/python/grpcio/tests/unit/beta/_beta_features_test.py
new file mode 100644
index 0000000000..ea44177b49
--- /dev/null
+++ b/src/python/grpcio/tests/unit/beta/_beta_features_test.py
@@ -0,0 +1,343 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests Face interface compliance of the gRPC Python Beta API."""
+
+import threading
+import unittest
+
+from grpc.beta import implementations
+from grpc.beta import interfaces
+from grpc.framework.common import cardinality
+from grpc.framework.interfaces.face import utilities
+from tests.unit import resources
+from tests.unit.beta import test_utilities
+from tests.unit.framework.common import test_constants
+
+_SERVER_HOST_OVERRIDE = 'foo.test.google.fr'
+
+_PER_RPC_CREDENTIALS_METADATA_KEY = 'my-call-credentials-metadata-key'
+_PER_RPC_CREDENTIALS_METADATA_VALUE = 'my-call-credentials-metadata-value'
+
+_GROUP = 'group'
+_UNARY_UNARY = 'unary-unary'
+_UNARY_STREAM = 'unary-stream'
+_STREAM_UNARY = 'stream-unary'
+_STREAM_STREAM = 'stream-stream'
+
+_REQUEST = b'abc'
+_RESPONSE = b'123'
+
+
+class _Servicer(object):
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._peer = None
+ self._serviced = False
+
+ def unary_unary(self, request, context):
+ with self._condition:
+ self._request = request
+ self._peer = context.protocol_context().peer()
+ self._invocation_metadata = context.invocation_metadata()
+ context.protocol_context().disable_next_response_compression()
+ self._serviced = True
+ self._condition.notify_all()
+ return _RESPONSE
+
+ def unary_stream(self, request, context):
+ with self._condition:
+ self._request = request
+ self._peer = context.protocol_context().peer()
+ self._invocation_metadata = context.invocation_metadata()
+ context.protocol_context().disable_next_response_compression()
+ self._serviced = True
+ self._condition.notify_all()
+ return
+ yield
+
+ def stream_unary(self, request_iterator, context):
+ for request in request_iterator:
+ self._request = request
+ with self._condition:
+ self._peer = context.protocol_context().peer()
+ self._invocation_metadata = context.invocation_metadata()
+ context.protocol_context().disable_next_response_compression()
+ self._serviced = True
+ self._condition.notify_all()
+ return _RESPONSE
+
+ def stream_stream(self, request_iterator, context):
+ for request in request_iterator:
+ with self._condition:
+ self._peer = context.protocol_context().peer()
+ context.protocol_context().disable_next_response_compression()
+ yield _RESPONSE
+ with self._condition:
+ self._invocation_metadata = context.invocation_metadata()
+ self._serviced = True
+ self._condition.notify_all()
+
+ def peer(self):
+ with self._condition:
+ return self._peer
+
+ def block_until_serviced(self):
+ with self._condition:
+ while not self._serviced:
+ self._condition.wait()
+
+
+class _BlockingIterator(object):
+
+ def __init__(self, upstream):
+ self._condition = threading.Condition()
+ self._upstream = upstream
+ self._allowed = []
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ with self._condition:
+ while True:
+ if self._allowed is None:
+ raise StopIteration()
+ elif self._allowed:
+ return self._allowed.pop(0)
+ else:
+ self._condition.wait()
+
+ def allow(self):
+ with self._condition:
+ try:
+ self._allowed.append(next(self._upstream))
+ except StopIteration:
+ self._allowed = None
+ self._condition.notify_all()
+
+
+def _metadata_plugin(context, callback):
+ callback([(_PER_RPC_CREDENTIALS_METADATA_KEY,
+ _PER_RPC_CREDENTIALS_METADATA_VALUE)], None)
+
+
+class BetaFeaturesTest(unittest.TestCase):
+
+ def setUp(self):
+ self._servicer = _Servicer()
+ method_implementations = {
+ (_GROUP, _UNARY_UNARY):
+ utilities.unary_unary_inline(self._servicer.unary_unary),
+ (_GROUP, _UNARY_STREAM):
+ utilities.unary_stream_inline(self._servicer.unary_stream),
+ (_GROUP, _STREAM_UNARY):
+ utilities.stream_unary_inline(self._servicer.stream_unary),
+ (_GROUP, _STREAM_STREAM):
+ utilities.stream_stream_inline(self._servicer.stream_stream),
+ }
+
+ cardinalities = {
+ _UNARY_UNARY: cardinality.Cardinality.UNARY_UNARY,
+ _UNARY_STREAM: cardinality.Cardinality.UNARY_STREAM,
+ _STREAM_UNARY: cardinality.Cardinality.STREAM_UNARY,
+ _STREAM_STREAM: cardinality.Cardinality.STREAM_STREAM,
+ }
+
+ server_options = implementations.server_options(
+ thread_pool_size=test_constants.POOL_SIZE)
+ self._server = implementations.server(
+ method_implementations, options=server_options)
+ server_credentials = implementations.ssl_server_credentials(
+ [(resources.private_key(), resources.certificate_chain(),),])
+ port = self._server.add_secure_port('[::]:0', server_credentials)
+ self._server.start()
+ self._channel_credentials = implementations.ssl_channel_credentials(
+ resources.test_root_certificates(), None, None)
+ self._call_credentials = implementations.metadata_call_credentials(
+ _metadata_plugin)
+ channel = test_utilities.not_really_secure_channel(
+ 'localhost', port, self._channel_credentials, _SERVER_HOST_OVERRIDE)
+ stub_options = implementations.stub_options(
+ thread_pool_size=test_constants.POOL_SIZE)
+ self._dynamic_stub = implementations.dynamic_stub(
+ channel, _GROUP, cardinalities, options=stub_options)
+
+ def tearDown(self):
+ self._dynamic_stub = None
+ self._server.stop(test_constants.SHORT_TIMEOUT).wait()
+
+ def test_unary_unary(self):
+ call_options = interfaces.grpc_call_options(
+ disable_compression=True, credentials=self._call_credentials)
+ response = getattr(self._dynamic_stub, _UNARY_UNARY)(
+ _REQUEST, test_constants.LONG_TIMEOUT, protocol_options=call_options)
+ self.assertEqual(_RESPONSE, response)
+ self.assertIsNotNone(self._servicer.peer())
+ invocation_metadata = [(metadatum.key, metadatum.value) for metadatum in
+ self._servicer._invocation_metadata]
+ self.assertIn(
+ (_PER_RPC_CREDENTIALS_METADATA_KEY,
+ _PER_RPC_CREDENTIALS_METADATA_VALUE),
+ invocation_metadata)
+
+ def test_unary_stream(self):
+ call_options = interfaces.grpc_call_options(
+ disable_compression=True, credentials=self._call_credentials)
+ response_iterator = getattr(self._dynamic_stub, _UNARY_STREAM)(
+ _REQUEST, test_constants.LONG_TIMEOUT, protocol_options=call_options)
+ self._servicer.block_until_serviced()
+ self.assertIsNotNone(self._servicer.peer())
+ invocation_metadata = [(metadatum.key, metadatum.value) for metadatum in
+ self._servicer._invocation_metadata]
+ self.assertIn(
+ (_PER_RPC_CREDENTIALS_METADATA_KEY,
+ _PER_RPC_CREDENTIALS_METADATA_VALUE),
+ invocation_metadata)
+
+ def test_stream_unary(self):
+ call_options = interfaces.grpc_call_options(
+ credentials=self._call_credentials)
+ request_iterator = _BlockingIterator(iter((_REQUEST,)))
+ response_future = getattr(self._dynamic_stub, _STREAM_UNARY).future(
+ request_iterator, test_constants.LONG_TIMEOUT,
+ protocol_options=call_options)
+ response_future.protocol_context().disable_next_request_compression()
+ request_iterator.allow()
+ response_future.protocol_context().disable_next_request_compression()
+ request_iterator.allow()
+ self._servicer.block_until_serviced()
+ self.assertIsNotNone(self._servicer.peer())
+ self.assertEqual(_RESPONSE, response_future.result())
+ invocation_metadata = [(metadatum.key, metadatum.value) for metadatum in
+ self._servicer._invocation_metadata]
+ self.assertIn(
+ (_PER_RPC_CREDENTIALS_METADATA_KEY,
+ _PER_RPC_CREDENTIALS_METADATA_VALUE),
+ invocation_metadata)
+
+ def test_stream_stream(self):
+ call_options = interfaces.grpc_call_options(
+ credentials=self._call_credentials)
+ request_iterator = _BlockingIterator(iter((_REQUEST,)))
+ response_iterator = getattr(self._dynamic_stub, _STREAM_STREAM)(
+ request_iterator, test_constants.SHORT_TIMEOUT,
+ protocol_options=call_options)
+ response_iterator.protocol_context().disable_next_request_compression()
+ request_iterator.allow()
+ response = next(response_iterator)
+ response_iterator.protocol_context().disable_next_request_compression()
+ request_iterator.allow()
+ self._servicer.block_until_serviced()
+ self.assertIsNotNone(self._servicer.peer())
+ self.assertEqual(_RESPONSE, response)
+ invocation_metadata = [(metadatum.key, metadatum.value) for metadatum in
+ self._servicer._invocation_metadata]
+ self.assertIn(
+ (_PER_RPC_CREDENTIALS_METADATA_KEY,
+ _PER_RPC_CREDENTIALS_METADATA_VALUE),
+ invocation_metadata)
+
+
+class ContextManagementAndLifecycleTest(unittest.TestCase):
+
+ def setUp(self):
+ self._servicer = _Servicer()
+ self._method_implementations = {
+ (_GROUP, _UNARY_UNARY):
+ utilities.unary_unary_inline(self._servicer.unary_unary),
+ (_GROUP, _UNARY_STREAM):
+ utilities.unary_stream_inline(self._servicer.unary_stream),
+ (_GROUP, _STREAM_UNARY):
+ utilities.stream_unary_inline(self._servicer.stream_unary),
+ (_GROUP, _STREAM_STREAM):
+ utilities.stream_stream_inline(self._servicer.stream_stream),
+ }
+
+ self._cardinalities = {
+ _UNARY_UNARY: cardinality.Cardinality.UNARY_UNARY,
+ _UNARY_STREAM: cardinality.Cardinality.UNARY_STREAM,
+ _STREAM_UNARY: cardinality.Cardinality.STREAM_UNARY,
+ _STREAM_STREAM: cardinality.Cardinality.STREAM_STREAM,
+ }
+
+ self._server_options = implementations.server_options(
+ thread_pool_size=test_constants.POOL_SIZE)
+ self._server_credentials = implementations.ssl_server_credentials(
+ [(resources.private_key(), resources.certificate_chain(),),])
+ self._channel_credentials = implementations.ssl_channel_credentials(
+ resources.test_root_certificates(), None, None)
+ self._stub_options = implementations.stub_options(
+ thread_pool_size=test_constants.POOL_SIZE)
+
+ def test_stub_context(self):
+ server = implementations.server(
+ self._method_implementations, options=self._server_options)
+ port = server.add_secure_port('[::]:0', self._server_credentials)
+ server.start()
+
+ channel = test_utilities.not_really_secure_channel(
+ 'localhost', port, self._channel_credentials, _SERVER_HOST_OVERRIDE)
+ dynamic_stub = implementations.dynamic_stub(
+ channel, _GROUP, self._cardinalities, options=self._stub_options)
+ for _ in range(100):
+ with dynamic_stub:
+ pass
+ for _ in range(10):
+ with dynamic_stub:
+ call_options = interfaces.grpc_call_options(
+ disable_compression=True)
+ response = getattr(dynamic_stub, _UNARY_UNARY)(
+ _REQUEST, test_constants.LONG_TIMEOUT,
+ protocol_options=call_options)
+ self.assertEqual(_RESPONSE, response)
+ self.assertIsNotNone(self._servicer.peer())
+
+ server.stop(test_constants.SHORT_TIMEOUT).wait()
+
+ def test_server_lifecycle(self):
+ for _ in range(100):
+ server = implementations.server(
+ self._method_implementations, options=self._server_options)
+ port = server.add_secure_port('[::]:0', self._server_credentials)
+ server.start()
+ server.stop(test_constants.SHORT_TIMEOUT).wait()
+ for _ in range(100):
+ server = implementations.server(
+ self._method_implementations, options=self._server_options)
+ server.add_secure_port('[::]:0', self._server_credentials)
+ server.add_insecure_port('[::]:0')
+ with server:
+ server.stop(test_constants.SHORT_TIMEOUT)
+ server.stop(test_constants.SHORT_TIMEOUT)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/beta/_connectivity_channel_test.py b/src/python/grpcio/tests/unit/beta/_connectivity_channel_test.py
new file mode 100644
index 0000000000..5dc8720639
--- /dev/null
+++ b/src/python/grpcio/tests/unit/beta/_connectivity_channel_test.py
@@ -0,0 +1,191 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests of grpc.beta._connectivity_channel."""
+
+import threading
+import time
+import unittest
+
+from grpc._adapter import _low
+from grpc._adapter import _types
+from grpc.beta import _connectivity_channel
+from grpc.beta import interfaces
+from tests.unit.framework.common import test_constants
+
+
+def _drive_completion_queue(completion_queue):
+ while True:
+ event = completion_queue.next(time.time() + 24 * 60 * 60)
+ if event.type == _types.EventType.QUEUE_SHUTDOWN:
+ break
+
+
+class _Callback(object):
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._connectivities = []
+
+ def update(self, connectivity):
+ with self._condition:
+ self._connectivities.append(connectivity)
+ self._condition.notify()
+
+ def connectivities(self):
+ with self._condition:
+ return tuple(self._connectivities)
+
+ def block_until_connectivities_satisfy(self, predicate):
+ with self._condition:
+ while True:
+ connectivities = tuple(self._connectivities)
+ if predicate(connectivities):
+ return connectivities
+ else:
+ self._condition.wait()
+
+
+class ChannelConnectivityTest(unittest.TestCase):
+
+ def test_lonely_channel_connectivity(self):
+ low_channel = _low.Channel('localhost:12345', ())
+ callback = _Callback()
+
+ connectivity_channel = _connectivity_channel.ConnectivityChannel(
+ low_channel)
+ connectivity_channel.subscribe(callback.update, try_to_connect=False)
+ first_connectivities = callback.block_until_connectivities_satisfy(bool)
+ connectivity_channel.subscribe(callback.update, try_to_connect=True)
+ second_connectivities = callback.block_until_connectivities_satisfy(
+ lambda connectivities: 2 <= len(connectivities))
+ # Wait for a connection that will never happen.
+ time.sleep(test_constants.SHORT_TIMEOUT)
+ third_connectivities = callback.connectivities()
+ connectivity_channel.unsubscribe(callback.update)
+ fourth_connectivities = callback.connectivities()
+ connectivity_channel.unsubscribe(callback.update)
+ fifth_connectivities = callback.connectivities()
+
+ self.assertSequenceEqual(
+ (interfaces.ChannelConnectivity.IDLE,), first_connectivities)
+ self.assertNotIn(
+ interfaces.ChannelConnectivity.READY, second_connectivities)
+ self.assertNotIn(
+ interfaces.ChannelConnectivity.READY, third_connectivities)
+ self.assertNotIn(
+ interfaces.ChannelConnectivity.READY, fourth_connectivities)
+ self.assertNotIn(
+ interfaces.ChannelConnectivity.READY, fifth_connectivities)
+
+ def test_immediately_connectable_channel_connectivity(self):
+ server_completion_queue = _low.CompletionQueue()
+ server = _low.Server(server_completion_queue, [])
+ port = server.add_http2_port('[::]:0')
+ server.start()
+ server_completion_queue_thread = threading.Thread(
+ target=_drive_completion_queue, args=(server_completion_queue,))
+ server_completion_queue_thread.start()
+ low_channel = _low.Channel('localhost:%d' % port, ())
+ first_callback = _Callback()
+ second_callback = _Callback()
+
+ connectivity_channel = _connectivity_channel.ConnectivityChannel(
+ low_channel)
+ connectivity_channel.subscribe(first_callback.update, try_to_connect=False)
+ first_connectivities = first_callback.block_until_connectivities_satisfy(
+ bool)
+ # Wait for a connection that will never happen because try_to_connect=True
+ # has not yet been passed.
+ time.sleep(test_constants.SHORT_TIMEOUT)
+ second_connectivities = first_callback.connectivities()
+ connectivity_channel.subscribe(second_callback.update, try_to_connect=True)
+ third_connectivities = first_callback.block_until_connectivities_satisfy(
+ lambda connectivities: 2 <= len(connectivities))
+ fourth_connectivities = second_callback.block_until_connectivities_satisfy(
+ bool)
+ # Wait for a connection that will happen (or may already have happened).
+ first_callback.block_until_connectivities_satisfy(
+ lambda connectivities:
+ interfaces.ChannelConnectivity.READY in connectivities)
+ second_callback.block_until_connectivities_satisfy(
+ lambda connectivities:
+ interfaces.ChannelConnectivity.READY in connectivities)
+ connectivity_channel.unsubscribe(first_callback.update)
+ connectivity_channel.unsubscribe(second_callback.update)
+
+ server.shutdown()
+ server_completion_queue.shutdown()
+ server_completion_queue_thread.join()
+
+ self.assertSequenceEqual(
+ (interfaces.ChannelConnectivity.IDLE,), first_connectivities)
+ self.assertSequenceEqual(
+ (interfaces.ChannelConnectivity.IDLE,), second_connectivities)
+ self.assertNotIn(
+ interfaces.ChannelConnectivity.TRANSIENT_FAILURE, third_connectivities)
+ self.assertNotIn(
+ interfaces.ChannelConnectivity.FATAL_FAILURE, third_connectivities)
+ self.assertNotIn(
+ interfaces.ChannelConnectivity.TRANSIENT_FAILURE,
+ fourth_connectivities)
+ self.assertNotIn(
+ interfaces.ChannelConnectivity.FATAL_FAILURE, fourth_connectivities)
+
+ def test_reachable_then_unreachable_channel_connectivity(self):
+ server_completion_queue = _low.CompletionQueue()
+ server = _low.Server(server_completion_queue, [])
+ port = server.add_http2_port('[::]:0')
+ server.start()
+ server_completion_queue_thread = threading.Thread(
+ target=_drive_completion_queue, args=(server_completion_queue,))
+ server_completion_queue_thread.start()
+ low_channel = _low.Channel('localhost:%d' % port, ())
+ callback = _Callback()
+
+ connectivity_channel = _connectivity_channel.ConnectivityChannel(
+ low_channel)
+ connectivity_channel.subscribe(callback.update, try_to_connect=True)
+ callback.block_until_connectivities_satisfy(
+ lambda connectivities:
+ interfaces.ChannelConnectivity.READY in connectivities)
+ # Now take down the server and confirm that channel readiness is repudiated.
+ server.shutdown()
+ callback.block_until_connectivities_satisfy(
+ lambda connectivities:
+ connectivities[-1] is not interfaces.ChannelConnectivity.READY)
+ connectivity_channel.unsubscribe(callback.update)
+
+ server.shutdown()
+ server_completion_queue.shutdown()
+ server_completion_queue_thread.join()
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/beta/_face_interface_test.py b/src/python/grpcio/tests/unit/beta/_face_interface_test.py
new file mode 100644
index 0000000000..1c21dfd03d
--- /dev/null
+++ b/src/python/grpcio/tests/unit/beta/_face_interface_test.py
@@ -0,0 +1,138 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests Face interface compliance of the gRPC Python Beta API."""
+
+import collections
+import unittest
+
+from grpc.beta import implementations
+from grpc.beta import interfaces
+from tests.unit import resources
+from tests.unit import test_common as grpc_test_common
+from tests.unit.beta import test_utilities
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.face import test_cases
+from tests.unit.framework.interfaces.face import test_interfaces
+
+_SERVER_HOST_OVERRIDE = 'foo.test.google.fr'
+
+
+class _SerializationBehaviors(
+ collections.namedtuple(
+ '_SerializationBehaviors',
+ ('request_serializers', 'request_deserializers', 'response_serializers',
+ 'response_deserializers',))):
+ pass
+
+
+def _serialization_behaviors_from_test_methods(test_methods):
+ request_serializers = {}
+ request_deserializers = {}
+ response_serializers = {}
+ response_deserializers = {}
+ for (group, method), test_method in test_methods.iteritems():
+ request_serializers[group, method] = test_method.serialize_request
+ request_deserializers[group, method] = test_method.deserialize_request
+ response_serializers[group, method] = test_method.serialize_response
+ response_deserializers[group, method] = test_method.deserialize_response
+ return _SerializationBehaviors(
+ request_serializers, request_deserializers, response_serializers,
+ response_deserializers)
+
+
+class _Implementation(test_interfaces.Implementation):
+
+ def instantiate(
+ self, methods, method_implementations, multi_method_implementation):
+ serialization_behaviors = _serialization_behaviors_from_test_methods(
+ methods)
+ # TODO(nathaniel): Add a "groups" attribute to _digest.TestServiceDigest.
+ service = next(iter(methods))[0]
+ # TODO(nathaniel): Add a "cardinalities_by_group" attribute to
+ # _digest.TestServiceDigest.
+ cardinalities = {
+ method: method_object.cardinality()
+ for (group, method), method_object in methods.iteritems()}
+
+ server_options = implementations.server_options(
+ request_deserializers=serialization_behaviors.request_deserializers,
+ response_serializers=serialization_behaviors.response_serializers,
+ thread_pool_size=test_constants.POOL_SIZE)
+ server = implementations.server(
+ method_implementations, options=server_options)
+ server_credentials = implementations.ssl_server_credentials(
+ [(resources.private_key(), resources.certificate_chain(),),])
+ port = server.add_secure_port('[::]:0', server_credentials)
+ server.start()
+ channel_credentials = implementations.ssl_channel_credentials(
+ resources.test_root_certificates(), None, None)
+ channel = test_utilities.not_really_secure_channel(
+ 'localhost', port, channel_credentials, _SERVER_HOST_OVERRIDE)
+ stub_options = implementations.stub_options(
+ request_serializers=serialization_behaviors.request_serializers,
+ response_deserializers=serialization_behaviors.response_deserializers,
+ thread_pool_size=test_constants.POOL_SIZE)
+ generic_stub = implementations.generic_stub(channel, options=stub_options)
+ dynamic_stub = implementations.dynamic_stub(
+ channel, service, cardinalities, options=stub_options)
+ return generic_stub, {service: dynamic_stub}, server
+
+ def destantiate(self, memo):
+ memo.stop(test_constants.SHORT_TIMEOUT).wait()
+
+ def invocation_metadata(self):
+ return grpc_test_common.INVOCATION_INITIAL_METADATA
+
+ def initial_metadata(self):
+ return grpc_test_common.SERVICE_INITIAL_METADATA
+
+ def terminal_metadata(self):
+ return grpc_test_common.SERVICE_TERMINAL_METADATA
+
+ def code(self):
+ return interfaces.StatusCode.OK
+
+ def details(self):
+ return grpc_test_common.DETAILS
+
+ def metadata_transmitted(self, original_metadata, transmitted_metadata):
+ return original_metadata is None or grpc_test_common.metadata_transmitted(
+ original_metadata, transmitted_metadata)
+
+
+def load_tests(loader, tests, pattern):
+ return unittest.TestSuite(
+ tests=tuple(
+ loader.loadTestsFromTestCase(test_case_class)
+ for test_case_class in test_cases.test_cases(_Implementation())))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/beta/_not_found_test.py b/src/python/grpcio/tests/unit/beta/_not_found_test.py
new file mode 100644
index 0000000000..44fcd1e13c
--- /dev/null
+++ b/src/python/grpcio/tests/unit/beta/_not_found_test.py
@@ -0,0 +1,75 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests of RPC-method-not-found behavior."""
+
+import unittest
+
+from grpc.beta import implementations
+from grpc.beta import interfaces
+from grpc.framework.interfaces.face import face
+from tests.unit.framework.common import test_constants
+
+
+class NotFoundTest(unittest.TestCase):
+
+ def setUp(self):
+ self._server = implementations.server({})
+ port = self._server.add_insecure_port('[::]:0')
+ channel = implementations.insecure_channel('localhost', port)
+ self._generic_stub = implementations.generic_stub(channel)
+ self._server.start()
+
+ def tearDown(self):
+ self._server.stop(0).wait()
+ self._generic_stub = None
+
+ def test_blocking_unary_unary_not_found(self):
+ with self.assertRaises(face.LocalError) as exception_assertion_context:
+ self._generic_stub.blocking_unary_unary(
+ 'groop', 'meffod', b'abc', test_constants.LONG_TIMEOUT,
+ with_call=True)
+ self.assertIs(
+ exception_assertion_context.exception.code,
+ interfaces.StatusCode.UNIMPLEMENTED)
+
+ def test_future_stream_unary_not_found(self):
+ rpc_future = self._generic_stub.future_stream_unary(
+ 'grupe', 'mevvod', b'def', test_constants.LONG_TIMEOUT)
+ with self.assertRaises(face.LocalError) as exception_assertion_context:
+ rpc_future.result()
+ self.assertIs(
+ exception_assertion_context.exception.code,
+ interfaces.StatusCode.UNIMPLEMENTED)
+ self.assertIs(
+ rpc_future.exception().code, interfaces.StatusCode.UNIMPLEMENTED)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/beta/_utilities_test.py b/src/python/grpcio/tests/unit/beta/_utilities_test.py
new file mode 100644
index 0000000000..08ce98e751
--- /dev/null
+++ b/src/python/grpcio/tests/unit/beta/_utilities_test.py
@@ -0,0 +1,123 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests of grpc.beta.utilities."""
+
+import threading
+import time
+import unittest
+
+from grpc._adapter import _low
+from grpc._adapter import _types
+from grpc.beta import implementations
+from grpc.beta import utilities
+from grpc.framework.foundation import future
+from tests.unit.framework.common import test_constants
+
+
+def _drive_completion_queue(completion_queue):
+ while True:
+ event = completion_queue.next(time.time() + 24 * 60 * 60)
+ if event.type == _types.EventType.QUEUE_SHUTDOWN:
+ break
+
+
+class _Callback(object):
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._value = None
+
+ def accept_value(self, value):
+ with self._condition:
+ self._value = value
+ self._condition.notify_all()
+
+ def block_until_called(self):
+ with self._condition:
+ while self._value is None:
+ self._condition.wait()
+ return self._value
+
+
+class ChannelConnectivityTest(unittest.TestCase):
+
+ def test_lonely_channel_connectivity(self):
+ channel = implementations.insecure_channel('localhost', 12345)
+ callback = _Callback()
+
+ ready_future = utilities.channel_ready_future(channel)
+ ready_future.add_done_callback(callback.accept_value)
+ with self.assertRaises(future.TimeoutError):
+ ready_future.result(test_constants.SHORT_TIMEOUT)
+ self.assertFalse(ready_future.cancelled())
+ self.assertFalse(ready_future.done())
+ self.assertTrue(ready_future.running())
+ ready_future.cancel()
+ value_passed_to_callback = callback.block_until_called()
+ self.assertIs(ready_future, value_passed_to_callback)
+ self.assertTrue(ready_future.cancelled())
+ self.assertTrue(ready_future.done())
+ self.assertFalse(ready_future.running())
+
+ def test_immediately_connectable_channel_connectivity(self):
+ server_completion_queue = _low.CompletionQueue()
+ server = _low.Server(server_completion_queue, [])
+ port = server.add_http2_port('[::]:0')
+ server.start()
+ server_completion_queue_thread = threading.Thread(
+ target=_drive_completion_queue, args=(server_completion_queue,))
+ server_completion_queue_thread.start()
+ channel = implementations.insecure_channel('localhost', port)
+ callback = _Callback()
+
+ try:
+ ready_future = utilities.channel_ready_future(channel)
+ ready_future.add_done_callback(callback.accept_value)
+ self.assertIsNone(
+ ready_future.result(test_constants.SHORT_TIMEOUT))
+ value_passed_to_callback = callback.block_until_called()
+ self.assertIs(ready_future, value_passed_to_callback)
+ self.assertFalse(ready_future.cancelled())
+ self.assertTrue(ready_future.done())
+ self.assertFalse(ready_future.running())
+ # Cancellation after maturity has no effect.
+ ready_future.cancel()
+ self.assertFalse(ready_future.cancelled())
+ self.assertTrue(ready_future.done())
+ self.assertFalse(ready_future.running())
+ finally:
+ ready_future.cancel()
+ server.shutdown()
+ server_completion_queue.shutdown()
+ server_completion_queue_thread.join()
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/beta/test_utilities.py b/src/python/grpcio/tests/unit/beta/test_utilities.py
new file mode 100644
index 0000000000..0313e06a93
--- /dev/null
+++ b/src/python/grpcio/tests/unit/beta/test_utilities.py
@@ -0,0 +1,56 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Test-appropriate entry points into the gRPC Python Beta API."""
+
+from grpc._adapter import _intermediary_low
+from grpc.beta import implementations
+
+
+def not_really_secure_channel(
+ host, port, channel_credentials, server_host_override):
+ """Creates an insecure Channel to a remote host.
+
+ Args:
+ host: The name of the remote host to which to connect.
+ port: The port of the remote host to which to connect.
+ channel_credentials: The implementations.ChannelCredentials with which to
+ connect.
+ server_host_override: The target name used for SSL host name checking.
+
+ Returns:
+ An implementations.Channel to the remote host through which RPCs may be
+ conducted.
+ """
+ hostport = '%s:%d' % (host, port)
+ intermediary_low_channel = _intermediary_low.Channel(
+ hostport, channel_credentials._low_credentials,
+ server_host_override=server_host_override)
+ return implementations.Channel(
+ intermediary_low_channel._internal, intermediary_low_channel)
diff --git a/src/python/grpcio/tests/unit/credentials/README b/src/python/grpcio/tests/unit/credentials/README
new file mode 100644
index 0000000000..cb20dcb49f
--- /dev/null
+++ b/src/python/grpcio/tests/unit/credentials/README
@@ -0,0 +1 @@
+These are test keys *NOT* to be used in production.
diff --git a/src/python/grpcio/tests/unit/credentials/ca.pem b/src/python/grpcio/tests/unit/credentials/ca.pem
new file mode 100755
index 0000000000..6c8511a73c
--- /dev/null
+++ b/src/python/grpcio/tests/unit/credentials/ca.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
+Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
+YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
+BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
+g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
+Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
+sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
+oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
+Dfcog5wrJytaQ6UA0wE=
+-----END CERTIFICATE-----
diff --git a/src/python/grpcio/tests/unit/credentials/server1.key b/src/python/grpcio/tests/unit/credentials/server1.key
new file mode 100755
index 0000000000..143a5b8765
--- /dev/null
+++ b/src/python/grpcio/tests/unit/credentials/server1.key
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
+M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
+3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
+AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
+V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
+tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
+dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
+K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
+81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
+DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
+aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
+ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
+XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
+F98XJ7tIFfJq
+-----END PRIVATE KEY-----
diff --git a/src/python/grpcio/tests/unit/credentials/server1.pem b/src/python/grpcio/tests/unit/credentials/server1.pem
new file mode 100755
index 0000000000..f3d43fcc5b
--- /dev/null
+++ b/src/python/grpcio/tests/unit/credentials/server1.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
+MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
+dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
+MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
+BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
+ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
+LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
+zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
+9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
+CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
+em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
+CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
+hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
+y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
+-----END CERTIFICATE-----
diff --git a/src/python/grpcio/tests/unit/framework/__init__.py b/src/python/grpcio/tests/unit/framework/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/_crust_over_core_face_interface_test.py b/src/python/grpcio/tests/unit/framework/_crust_over_core_face_interface_test.py
new file mode 100644
index 0000000000..360ecc95d5
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/_crust_over_core_face_interface_test.py
@@ -0,0 +1,111 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests Face interface compliance of the crust-over-core stack."""
+
+import collections
+import unittest
+
+from grpc.framework.core import implementations as core_implementations
+from grpc.framework.crust import implementations as crust_implementations
+from grpc.framework.foundation import logging_pool
+from grpc.framework.interfaces.links import utilities
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.face import test_cases
+from tests.unit.framework.interfaces.face import test_interfaces
+from tests.unit.framework.interfaces.links import test_utilities
+
+
+class _Implementation(test_interfaces.Implementation):
+
+ def instantiate(
+ self, methods, method_implementations, multi_method_implementation):
+ pool = logging_pool.pool(test_constants.POOL_SIZE)
+ servicer = crust_implementations.servicer(
+ method_implementations, multi_method_implementation, pool)
+
+ service_end_link = core_implementations.service_end_link(
+ servicer, test_constants.DEFAULT_TIMEOUT,
+ test_constants.MAXIMUM_TIMEOUT)
+ invocation_end_link = core_implementations.invocation_end_link()
+ invocation_end_link.join_link(service_end_link)
+ service_end_link.join_link(invocation_end_link)
+ service_end_link.start()
+ invocation_end_link.start()
+
+ generic_stub = crust_implementations.generic_stub(invocation_end_link, pool)
+ # TODO(nathaniel): Add a "groups" attribute to _digest.TestServiceDigest.
+ group = next(iter(methods))[0]
+ # TODO(nathaniel): Add a "cardinalities_by_group" attribute to
+ # _digest.TestServiceDigest.
+ cardinalities = {
+ method: method_object.cardinality()
+ for (group, method), method_object in methods.iteritems()}
+ dynamic_stub = crust_implementations.dynamic_stub(
+ invocation_end_link, group, cardinalities, pool)
+
+ return generic_stub, {group: dynamic_stub}, (
+ invocation_end_link, service_end_link, pool)
+
+ def destantiate(self, memo):
+ invocation_end_link, service_end_link, pool = memo
+ invocation_end_link.stop(0).wait()
+ service_end_link.stop(0).wait()
+ invocation_end_link.join_link(utilities.NULL_LINK)
+ service_end_link.join_link(utilities.NULL_LINK)
+ pool.shutdown(wait=True)
+
+ def invocation_metadata(self):
+ return object()
+
+ def initial_metadata(self):
+ return object()
+
+ def terminal_metadata(self):
+ return object()
+
+ def code(self):
+ return object()
+
+ def details(self):
+ return object()
+
+ def metadata_transmitted(self, original_metadata, transmitted_metadata):
+ return original_metadata is transmitted_metadata
+
+
+def load_tests(loader, tests, pattern):
+ return unittest.TestSuite(
+ tests=tuple(
+ loader.loadTestsFromTestCase(test_case_class)
+ for test_case_class in test_cases.test_cases(_Implementation())))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/framework/common/__init__.py b/src/python/grpcio/tests/unit/framework/common/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/common/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/common/test_constants.py b/src/python/grpcio/tests/unit/framework/common/test_constants.py
new file mode 100644
index 0000000000..e1d3c2709d
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/common/test_constants.py
@@ -0,0 +1,53 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Constants shared among tests throughout RPC Framework."""
+
+# Value for maximum duration in seconds that a test is allowed for its actual
+# behavioral logic, excluding all time spent deliberately waiting in the test.
+TIME_ALLOWANCE = 10
+# Value for maximum duration in seconds of RPCs that may time out as part of a
+# test.
+SHORT_TIMEOUT = 4
+# Absurdly large value for maximum duration in seconds for should-not-time-out
+# RPCs made during tests.
+LONG_TIMEOUT = 3000
+# Values to supply on construction of an object that will service RPCs; these
+# should not be used as the actual timeout values of any RPCs made during tests.
+DEFAULT_TIMEOUT = 300
+MAXIMUM_TIMEOUT = 3600
+
+# The number of payloads to transmit in streaming tests.
+STREAM_LENGTH = 200
+
+# The size of payloads to transmit in tests.
+PAYLOAD_SIZE = 256 * 1024 + 17
+
+# The size of thread pools to use in tests.
+POOL_SIZE = 10
diff --git a/src/python/grpcio/tests/unit/framework/common/test_control.py b/src/python/grpcio/tests/unit/framework/common/test_control.py
new file mode 100644
index 0000000000..8d6eba5c2c
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/common/test_control.py
@@ -0,0 +1,95 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Code for instructing systems under test to block or fail."""
+
+import abc
+import contextlib
+import threading
+
+
+class Defect(Exception):
+ """Simulates a programming defect raised into in a system under test.
+
+ Use of a standard exception type is too easily misconstrued as an actual
+ defect in either the test infrastructure or the system under test.
+ """
+
+
+class Control(object):
+ """An object that accepts program control from a system under test.
+
+ Systems under test passed a Control should call its control() method
+ frequently during execution. The control() method may block, raise an
+ exception, or do nothing, all according to the enclosing test's desire for
+ the system under test to simulate hanging, failing, or functioning.
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def control(self):
+ """Potentially does anything."""
+ raise NotImplementedError()
+
+
+class PauseFailControl(Control):
+ """A Control that can be used to pause or fail code under control."""
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._paused = False
+ self._fail = False
+
+ def control(self):
+ with self._condition:
+ if self._fail:
+ raise Defect()
+
+ while self._paused:
+ self._condition.wait()
+
+ @contextlib.contextmanager
+ def pause(self):
+ """Pauses code under control while controlling code is in context."""
+ with self._condition:
+ self._paused = True
+ yield
+ with self._condition:
+ self._paused = False
+ self._condition.notify_all()
+
+ @contextlib.contextmanager
+ def fail(self):
+ """Fails code under control while controlling code is in context."""
+ with self._condition:
+ self._fail = True
+ yield
+ with self._condition:
+ self._fail = False
diff --git a/src/python/grpcio/tests/unit/framework/common/test_coverage.py b/src/python/grpcio/tests/unit/framework/common/test_coverage.py
new file mode 100644
index 0000000000..a7ed3582c4
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/common/test_coverage.py
@@ -0,0 +1,116 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Governs coverage for tests of RPCs throughout RPC Framework."""
+
+import abc
+
+# This code is designed for use with the unittest module.
+# pylint: disable=invalid-name
+
+
+class Coverage(object):
+ """Specification of test coverage."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def testSuccessfulUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testSuccessfulUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testSuccessfulStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testSuccessfulStreamRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testSequentialInvocations(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testParallelInvocations(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testWaitingForSomeButNotAllParallelInvocations(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testCancelledUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testCancelledUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testCancelledStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testCancelledStreamRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testExpiredUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testExpiredUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testExpiredStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testExpiredStreamRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testFailedUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testFailedUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testFailedStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testFailedStreamRequestStreamResponse(self):
+ raise NotImplementedError()
diff --git a/src/python/grpcio/tests/unit/framework/core/__init__.py b/src/python/grpcio/tests/unit/framework/core/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/core/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/core/_base_interface_test.py b/src/python/grpcio/tests/unit/framework/core/_base_interface_test.py
new file mode 100644
index 0000000000..1310292306
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/core/_base_interface_test.py
@@ -0,0 +1,96 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests the RPC Framework Core's implementation of the Base interface."""
+
+import logging
+import random
+import time
+import unittest
+
+from grpc.framework.core import implementations
+from grpc.framework.interfaces.base import utilities
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.base import test_cases
+from tests.unit.framework.interfaces.base import test_interfaces
+
+
+class _Implementation(test_interfaces.Implementation):
+
+ def __init__(self):
+ self._invocation_initial_metadata = object()
+ self._service_initial_metadata = object()
+ self._invocation_terminal_metadata = object()
+ self._service_terminal_metadata = object()
+
+ def instantiate(self, serializations, servicer):
+ invocation = implementations.invocation_end_link()
+ service = implementations.service_end_link(
+ servicer, test_constants.DEFAULT_TIMEOUT,
+ test_constants.MAXIMUM_TIMEOUT)
+ invocation.join_link(service)
+ service.join_link(invocation)
+ return invocation, service, None
+
+ def destantiate(self, memo):
+ pass
+
+ def invocation_initial_metadata(self):
+ return self._invocation_initial_metadata
+
+ def service_initial_metadata(self):
+ return self._service_initial_metadata
+
+ def invocation_completion(self):
+ return utilities.completion(self._invocation_terminal_metadata, None, None)
+
+ def service_completion(self):
+ return utilities.completion(self._service_terminal_metadata, None, None)
+
+ def metadata_transmitted(self, original_metadata, transmitted_metadata):
+ return transmitted_metadata is original_metadata
+
+ def completion_transmitted(self, original_completion, transmitted_completion):
+ return (
+ (original_completion.terminal_metadata is
+ transmitted_completion.terminal_metadata) and
+ original_completion.code is transmitted_completion.code and
+ original_completion.message is transmitted_completion.message
+ )
+
+
+def load_tests(loader, tests, pattern):
+ return unittest.TestSuite(
+ tests=tuple(
+ loader.loadTestsFromTestCase(test_case_class)
+ for test_case_class in test_cases.test_cases(_Implementation())))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/framework/face/__init__.py b/src/python/grpcio/tests/unit/framework/face/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/__init__.py b/src/python/grpcio/tests/unit/framework/face/testing/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/base_util.py b/src/python/grpcio/tests/unit/framework/face/testing/base_util.py
new file mode 100644
index 0000000000..1df1529b27
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/base_util.py
@@ -0,0 +1,102 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Utilities for creating Base-layer objects for use in Face-layer tests."""
+
+import abc
+
+# interfaces is referenced from specification in this module.
+from grpc.framework.base import util as _base_util
+from grpc.framework.base import implementations
+from grpc.framework.base import in_memory
+from grpc.framework.base import interfaces # pylint: disable=unused-import
+from grpc.framework.foundation import logging_pool
+
+_POOL_SIZE_LIMIT = 5
+
+_MAXIMUM_TIMEOUT = 90
+
+
+class LinkedPair(object):
+ """A Front and Back that are linked to one another.
+
+ Attributes:
+ front: An interfaces.Front.
+ back: An interfaces.Back.
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def shut_down(self):
+ """Shuts down this object and releases its resources."""
+ raise NotImplementedError()
+
+
+class _LinkedPair(LinkedPair):
+
+ def __init__(self, front, back, pools):
+ self.front = front
+ self.back = back
+ self._pools = pools
+
+ def shut_down(self):
+ _base_util.wait_for_idle(self.front)
+ _base_util.wait_for_idle(self.back)
+
+ for pool in self._pools:
+ pool.shutdown(wait=True)
+
+
+def linked_pair(servicer, default_timeout):
+ """Creates a Server and Stub linked together for use."""
+ link_pool = logging_pool.pool(_POOL_SIZE_LIMIT)
+ front_work_pool = logging_pool.pool(_POOL_SIZE_LIMIT)
+ front_transmission_pool = logging_pool.pool(_POOL_SIZE_LIMIT)
+ front_utility_pool = logging_pool.pool(_POOL_SIZE_LIMIT)
+ back_work_pool = logging_pool.pool(_POOL_SIZE_LIMIT)
+ back_transmission_pool = logging_pool.pool(_POOL_SIZE_LIMIT)
+ back_utility_pool = logging_pool.pool(_POOL_SIZE_LIMIT)
+ pools = (
+ link_pool,
+ front_work_pool, front_transmission_pool, front_utility_pool,
+ back_work_pool, back_transmission_pool, back_utility_pool)
+
+ link = in_memory.Link(link_pool)
+ front = implementations.front_link(
+ front_work_pool, front_transmission_pool, front_utility_pool)
+ back = implementations.back_link(
+ servicer, back_work_pool, back_transmission_pool, back_utility_pool,
+ default_timeout, _MAXIMUM_TIMEOUT)
+ front.join_rear_link(link)
+ link.join_fore_link(front)
+ back.join_fore_link(link)
+ link.join_rear_link(back)
+
+ return _LinkedPair(front, back, pools)
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/blocking_invocation_inline_service_test_case.py b/src/python/grpcio/tests/unit/framework/face/testing/blocking_invocation_inline_service_test_case.py
new file mode 100644
index 0000000000..0613516421
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/blocking_invocation_inline_service_test_case.py
@@ -0,0 +1,222 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A test to verify an implementation of the Face layer of RPC Framework."""
+
+# unittest is referenced from specification in this module.
+import abc
+import unittest # pylint: disable=unused-import
+
+from grpc.framework.face import exceptions
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.face.testing import control
+from tests.unit.framework.face.testing import coverage
+from tests.unit.framework.face.testing import digest
+from tests.unit.framework.face.testing import stock_service
+from tests.unit.framework.face.testing import test_case
+
+
+class BlockingInvocationInlineServiceTestCase(
+ test_case.FaceTestCase, coverage.BlockingCoverage):
+ """A test of the Face layer of RPC Framework.
+
+ Concrete subclasses must also extend unittest.TestCase.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ def setUp(self):
+ """See unittest.TestCase.setUp for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self.control = control.PauseFailControl()
+ self.digest = digest.digest(
+ stock_service.STOCK_TEST_SERVICE, self.control, None)
+
+ self.stub, self.memo = self.set_up_implementation(
+ self.digest.name, self.digest.methods,
+ self.digest.inline_method_implementations, None)
+
+ def tearDown(self):
+ """See unittest.TestCase.tearDown for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self.tear_down_implementation(self.memo)
+
+ def testSuccessfulUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ response = self.stub.blocking_value_in_value_out(
+ name, request, test_constants.LONG_TIMEOUT)
+
+ test_messages.verify(request, response, self)
+
+ def testSuccessfulUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ response_iterator = self.stub.inline_value_in_stream_out(
+ name, request, test_constants.LONG_TIMEOUT)
+ responses = list(response_iterator)
+
+ test_messages.verify(request, responses, self)
+
+ def testSuccessfulStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ response = self.stub.blocking_stream_in_value_out(
+ name, iter(requests), test_constants.LONG_TIMEOUT)
+
+ test_messages.verify(requests, response, self)
+
+ def testSuccessfulStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ response_iterator = self.stub.inline_stream_in_stream_out(
+ name, iter(requests), test_constants.LONG_TIMEOUT)
+ responses = list(response_iterator)
+
+ test_messages.verify(requests, responses, self)
+
+ def testSequentialInvocations(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ second_request = test_messages.request()
+
+ first_response = self.stub.blocking_value_in_value_out(
+ name, first_request, test_constants.SHORT_TIMEOUT)
+
+ test_messages.verify(first_request, first_response, self)
+
+ second_response = self.stub.blocking_value_in_value_out(
+ name, second_request, test_constants.SHORT_TIMEOUT)
+
+ test_messages.verify(second_request, second_response, self)
+
+ def testExpiredUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.pause(), self.assertRaises(
+ exceptions.ExpirationError):
+ multi_callable = self.stub.unary_unary_multi_callable(name)
+ multi_callable(request, test_constants.SHORT_TIMEOUT)
+
+ def testExpiredUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.pause(), self.assertRaises(
+ exceptions.ExpirationError):
+ response_iterator = self.stub.inline_value_in_stream_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+ list(response_iterator)
+
+ def testExpiredStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.pause(), self.assertRaises(
+ exceptions.ExpirationError):
+ multi_callable = self.stub.stream_unary_multi_callable(name)
+ multi_callable(iter(requests), test_constants.SHORT_TIMEOUT)
+
+ def testExpiredStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.pause(), self.assertRaises(
+ exceptions.ExpirationError):
+ response_iterator = self.stub.inline_stream_in_stream_out(
+ name, iter(requests), test_constants.SHORT_TIMEOUT)
+ list(response_iterator)
+
+ def testFailedUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.fail(), self.assertRaises(exceptions.ServicerError):
+ self.stub.blocking_value_in_value_out(name, request,
+ test_constants.SHORT_TIMEOUT)
+
+ def testFailedUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.fail(), self.assertRaises(exceptions.ServicerError):
+ response_iterator = self.stub.inline_value_in_stream_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+ list(response_iterator)
+
+ def testFailedStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.fail(), self.assertRaises(exceptions.ServicerError):
+ self.stub.blocking_stream_in_value_out(name, iter(requests),
+ test_constants.SHORT_TIMEOUT)
+
+ def testFailedStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.fail(), self.assertRaises(exceptions.ServicerError):
+ response_iterator = self.stub.inline_stream_in_stream_out(
+ name, iter(requests), test_constants.SHORT_TIMEOUT)
+ list(response_iterator)
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/callback.py b/src/python/grpcio/tests/unit/framework/face/testing/callback.py
new file mode 100644
index 0000000000..d0e63c8c56
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/callback.py
@@ -0,0 +1,94 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A utility useful in tests of asynchronous, event-driven interfaces."""
+
+import threading
+
+from grpc.framework.foundation import stream
+
+
+class Callback(stream.Consumer):
+ """A utility object useful in tests of asynchronous code."""
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._unary_response = None
+ self._streamed_responses = []
+ self._completed = False
+ self._abortion = None
+
+ def abort(self, abortion):
+ with self._condition:
+ self._abortion = abortion
+ self._condition.notify_all()
+
+ def complete(self, unary_response):
+ with self._condition:
+ self._unary_response = unary_response
+ self._completed = True
+ self._condition.notify_all()
+
+ def consume(self, streamed_response):
+ with self._condition:
+ self._streamed_responses.append(streamed_response)
+
+ def terminate(self):
+ with self._condition:
+ self._completed = True
+ self._condition.notify_all()
+
+ def consume_and_terminate(self, streamed_response):
+ with self._condition:
+ self._streamed_responses.append(streamed_response)
+ self._completed = True
+ self._condition.notify_all()
+
+ def block_until_terminated(self):
+ with self._condition:
+ while self._abortion is None and not self._completed:
+ self._condition.wait()
+
+ def response(self):
+ with self._condition:
+ if self._abortion is None:
+ return self._unary_response
+ else:
+ raise AssertionError('Aborted with abortion "%s"!' % self._abortion)
+
+ def responses(self):
+ with self._condition:
+ if self._abortion is None:
+ return list(self._streamed_responses)
+ else:
+ raise AssertionError('Aborted with abortion "%s"!' % self._abortion)
+
+ def abortion(self):
+ with self._condition:
+ return self._abortion
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/control.py b/src/python/grpcio/tests/unit/framework/face/testing/control.py
new file mode 100644
index 0000000000..3960c4e649
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/control.py
@@ -0,0 +1,87 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Code for instructing systems under test to block or fail."""
+
+import abc
+import contextlib
+import threading
+
+
+class Control(object):
+ """An object that accepts program control from a system under test.
+
+ Systems under test passed a Control should call its control() method
+ frequently during execution. The control() method may block, raise an
+ exception, or do nothing, all according to the enclosing test's desire for
+ the system under test to simulate hanging, failing, or functioning.
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def control(self):
+ """Potentially does anything."""
+ raise NotImplementedError()
+
+
+class PauseFailControl(Control):
+ """A Control that can be used to pause or fail code under control."""
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._paused = False
+ self._fail = False
+
+ def control(self):
+ with self._condition:
+ if self._fail:
+ raise ValueError()
+
+ while self._paused:
+ self._condition.wait()
+
+ @contextlib.contextmanager
+ def pause(self):
+ """Pauses code under control while controlling code is in context."""
+ with self._condition:
+ self._paused = True
+ yield
+ with self._condition:
+ self._paused = False
+ self._condition.notify_all()
+
+ @contextlib.contextmanager
+ def fail(self):
+ """Fails code under control while controlling code is in context."""
+ with self._condition:
+ self._fail = True
+ yield
+ with self._condition:
+ self._fail = False
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/coverage.py b/src/python/grpcio/tests/unit/framework/face/testing/coverage.py
new file mode 100644
index 0000000000..f3aca113fe
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/coverage.py
@@ -0,0 +1,123 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Governs coverage for the tests of the Face layer of RPC Framework."""
+
+import abc
+
+# These classes are only valid when inherited by unittest.TestCases.
+# pylint: disable=invalid-name
+
+
+class BlockingCoverage(object):
+ """Specification of test coverage for blocking behaviors."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def testSuccessfulUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testSuccessfulUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testSuccessfulStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testSuccessfulStreamRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testSequentialInvocations(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testExpiredUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testExpiredUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testExpiredStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testExpiredStreamRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testFailedUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testFailedUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testFailedStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testFailedStreamRequestStreamResponse(self):
+ raise NotImplementedError()
+
+
+class FullCoverage(BlockingCoverage):
+ """Specification of test coverage for non-blocking behaviors."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def testParallelInvocations(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testWaitingForSomeButNotAllParallelInvocations(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testCancelledUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testCancelledUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testCancelledStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def testCancelledStreamRequestStreamResponse(self):
+ raise NotImplementedError()
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/digest.py b/src/python/grpcio/tests/unit/framework/face/testing/digest.py
new file mode 100644
index 0000000000..39f28b9657
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/digest.py
@@ -0,0 +1,450 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Code for making a service.TestService more amenable to use in tests."""
+
+import collections
+import threading
+
+# testing_control, interfaces, and testing_service are referenced from
+# specification in this module.
+from grpc.framework.common import cardinality
+from grpc.framework.common import style
+from grpc.framework.face import exceptions
+from grpc.framework.face import interfaces as face_interfaces
+from grpc.framework.foundation import stream
+from grpc.framework.foundation import stream_util
+from tests.unit.framework.face.testing import control as testing_control # pylint: disable=unused-import
+from tests.unit.framework.face.testing import interfaces # pylint: disable=unused-import
+from tests.unit.framework.face.testing import service as testing_service # pylint: disable=unused-import
+
+_IDENTITY = lambda x: x
+
+
+class TestServiceDigest(
+ collections.namedtuple(
+ 'TestServiceDigest',
+ ['name',
+ 'methods',
+ 'inline_method_implementations',
+ 'event_method_implementations',
+ 'multi_method_implementation',
+ 'unary_unary_messages_sequences',
+ 'unary_stream_messages_sequences',
+ 'stream_unary_messages_sequences',
+ 'stream_stream_messages_sequences'])):
+ """A transformation of a service.TestService.
+
+ Attributes:
+ name: The RPC service name to be used in the test.
+ methods: A sequence of interfaces.Method objects describing the RPC
+ methods that will be called during the test.
+ inline_method_implementations: A dict from RPC method name to
+ face_interfaces.MethodImplementation object to be used in tests of
+ in-line calls to behaviors under test.
+ event_method_implementations: A dict from RPC method name to
+ face_interfaces.MethodImplementation object to be used in tests of
+ event-driven calls to behaviors under test.
+ multi_method_implementation: A face_interfaces.MultiMethodImplementation to
+ be used in tests of generic calls to behaviors under test.
+ unary_unary_messages_sequences: A dict from method name to sequence of
+ service.UnaryUnaryTestMessages objects to be used to test the method
+ with the given name.
+ unary_stream_messages_sequences: A dict from method name to sequence of
+ service.UnaryStreamTestMessages objects to be used to test the method
+ with the given name.
+ stream_unary_messages_sequences: A dict from method name to sequence of
+ service.StreamUnaryTestMessages objects to be used to test the method
+ with the given name.
+ stream_stream_messages_sequences: A dict from method name to sequence of
+ service.StreamStreamTestMessages objects to be used to test the
+ method with the given name.
+ serialization: A serial.Serialization object describing serialization
+ behaviors for all the RPC methods.
+ """
+
+
+class _BufferingConsumer(stream.Consumer):
+ """A trivial Consumer that dumps what it consumes in a user-mutable buffer."""
+
+ def __init__(self):
+ self.consumed = []
+ self.terminated = False
+
+ def consume(self, value):
+ self.consumed.append(value)
+
+ def terminate(self):
+ self.terminated = True
+
+ def consume_and_terminate(self, value):
+ self.consumed.append(value)
+ self.terminated = True
+
+
+class _InlineUnaryUnaryMethod(face_interfaces.MethodImplementation):
+
+ def __init__(self, unary_unary_test_method, control):
+ self._test_method = unary_unary_test_method
+ self._control = control
+
+ self.cardinality = cardinality.Cardinality.UNARY_UNARY
+ self.style = style.Service.INLINE
+
+ def unary_unary_inline(self, request, context):
+ response_list = []
+ self._test_method.service(
+ request, response_list.append, context, self._control)
+ return response_list.pop(0)
+
+
+class _EventUnaryUnaryMethod(face_interfaces.MethodImplementation):
+
+ def __init__(self, unary_unary_test_method, control, pool):
+ self._test_method = unary_unary_test_method
+ self._control = control
+ self._pool = pool
+
+ self.cardinality = cardinality.Cardinality.UNARY_UNARY
+ self.style = style.Service.EVENT
+
+ def unary_unary_event(self, request, response_callback, context):
+ if self._pool is None:
+ self._test_method.service(
+ request, response_callback, context, self._control)
+ else:
+ self._pool.submit(
+ self._test_method.service, request, response_callback, context,
+ self._control)
+
+
+class _InlineUnaryStreamMethod(face_interfaces.MethodImplementation):
+
+ def __init__(self, unary_stream_test_method, control):
+ self._test_method = unary_stream_test_method
+ self._control = control
+
+ self.cardinality = cardinality.Cardinality.UNARY_STREAM
+ self.style = style.Service.INLINE
+
+ def unary_stream_inline(self, request, context):
+ response_consumer = _BufferingConsumer()
+ self._test_method.service(
+ request, response_consumer, context, self._control)
+ for response in response_consumer.consumed:
+ yield response
+
+
+class _EventUnaryStreamMethod(face_interfaces.MethodImplementation):
+
+ def __init__(self, unary_stream_test_method, control, pool):
+ self._test_method = unary_stream_test_method
+ self._control = control
+ self._pool = pool
+
+ self.cardinality = cardinality.Cardinality.UNARY_STREAM
+ self.style = style.Service.EVENT
+
+ def unary_stream_event(self, request, response_consumer, context):
+ if self._pool is None:
+ self._test_method.service(
+ request, response_consumer, context, self._control)
+ else:
+ self._pool.submit(
+ self._test_method.service, request, response_consumer, context,
+ self._control)
+
+
+class _InlineStreamUnaryMethod(face_interfaces.MethodImplementation):
+
+ def __init__(self, stream_unary_test_method, control):
+ self._test_method = stream_unary_test_method
+ self._control = control
+
+ self.cardinality = cardinality.Cardinality.STREAM_UNARY
+ self.style = style.Service.INLINE
+
+ def stream_unary_inline(self, request_iterator, context):
+ response_list = []
+ request_consumer = self._test_method.service(
+ response_list.append, context, self._control)
+ for request in request_iterator:
+ request_consumer.consume(request)
+ request_consumer.terminate()
+ return response_list.pop(0)
+
+
+class _EventStreamUnaryMethod(face_interfaces.MethodImplementation):
+
+ def __init__(self, stream_unary_test_method, control, pool):
+ self._test_method = stream_unary_test_method
+ self._control = control
+ self._pool = pool
+
+ self.cardinality = cardinality.Cardinality.STREAM_UNARY
+ self.style = style.Service.EVENT
+
+ def stream_unary_event(self, response_callback, context):
+ request_consumer = self._test_method.service(
+ response_callback, context, self._control)
+ if self._pool is None:
+ return request_consumer
+ else:
+ return stream_util.ThreadSwitchingConsumer(request_consumer, self._pool)
+
+
+class _InlineStreamStreamMethod(face_interfaces.MethodImplementation):
+
+ def __init__(self, stream_stream_test_method, control):
+ self._test_method = stream_stream_test_method
+ self._control = control
+
+ self.cardinality = cardinality.Cardinality.STREAM_STREAM
+ self.style = style.Service.INLINE
+
+ def stream_stream_inline(self, request_iterator, context):
+ response_consumer = _BufferingConsumer()
+ request_consumer = self._test_method.service(
+ response_consumer, context, self._control)
+
+ for request in request_iterator:
+ request_consumer.consume(request)
+ while response_consumer.consumed:
+ yield response_consumer.consumed.pop(0)
+ response_consumer.terminate()
+
+
+class _EventStreamStreamMethod(face_interfaces.MethodImplementation):
+
+ def __init__(self, stream_stream_test_method, control, pool):
+ self._test_method = stream_stream_test_method
+ self._control = control
+ self._pool = pool
+
+ self.cardinality = cardinality.Cardinality.STREAM_STREAM
+ self.style = style.Service.EVENT
+
+ def stream_stream_event(self, response_consumer, context):
+ request_consumer = self._test_method.service(
+ response_consumer, context, self._control)
+ if self._pool is None:
+ return request_consumer
+ else:
+ return stream_util.ThreadSwitchingConsumer(request_consumer, self._pool)
+
+
+class _UnaryConsumer(stream.Consumer):
+ """A Consumer that only allows consumption of exactly one value."""
+
+ def __init__(self, action):
+ self._lock = threading.Lock()
+ self._action = action
+ self._consumed = False
+ self._terminated = False
+
+ def consume(self, value):
+ with self._lock:
+ if self._consumed:
+ raise ValueError('Unary consumer already consumed!')
+ elif self._terminated:
+ raise ValueError('Unary consumer already terminated!')
+ else:
+ self._consumed = True
+
+ self._action(value)
+
+ def terminate(self):
+ with self._lock:
+ if not self._consumed:
+ raise ValueError('Unary consumer hasn\'t yet consumed!')
+ elif self._terminated:
+ raise ValueError('Unary consumer already terminated!')
+ else:
+ self._terminated = True
+
+ def consume_and_terminate(self, value):
+ with self._lock:
+ if self._consumed:
+ raise ValueError('Unary consumer already consumed!')
+ elif self._terminated:
+ raise ValueError('Unary consumer already terminated!')
+ else:
+ self._consumed = True
+ self._terminated = True
+
+ self._action(value)
+
+
+class _UnaryUnaryAdaptation(object):
+
+ def __init__(self, unary_unary_test_method):
+ self._method = unary_unary_test_method
+
+ def service(self, response_consumer, context, control):
+ def action(request):
+ self._method.service(
+ request, response_consumer.consume_and_terminate, context, control)
+ return _UnaryConsumer(action)
+
+
+class _UnaryStreamAdaptation(object):
+
+ def __init__(self, unary_stream_test_method):
+ self._method = unary_stream_test_method
+
+ def service(self, response_consumer, context, control):
+ def action(request):
+ self._method.service(request, response_consumer, context, control)
+ return _UnaryConsumer(action)
+
+
+class _StreamUnaryAdaptation(object):
+
+ def __init__(self, stream_unary_test_method):
+ self._method = stream_unary_test_method
+
+ def service(self, response_consumer, context, control):
+ return self._method.service(
+ response_consumer.consume_and_terminate, context, control)
+
+
+class _MultiMethodImplementation(face_interfaces.MultiMethodImplementation):
+
+ def __init__(self, methods, control, pool):
+ self._methods = methods
+ self._control = control
+ self._pool = pool
+
+ def service(self, name, response_consumer, context):
+ method = self._methods.get(name, None)
+ if method is None:
+ raise exceptions.NoSuchMethodError(name)
+ elif self._pool is None:
+ return method(response_consumer, context, self._control)
+ else:
+ request_consumer = method(response_consumer, context, self._control)
+ return stream_util.ThreadSwitchingConsumer(request_consumer, self._pool)
+
+
+class _Assembly(
+ collections.namedtuple(
+ '_Assembly',
+ ['methods', 'inlines', 'events', 'adaptations', 'messages'])):
+ """An intermediate structure created when creating a TestServiceDigest."""
+
+
+def _assemble(
+ scenarios, names, inline_method_constructor, event_method_constructor,
+ adapter, control, pool):
+ """Creates an _Assembly from the given scenarios."""
+ methods = []
+ inlines = {}
+ events = {}
+ adaptations = {}
+ messages = {}
+ for name, scenario in scenarios.iteritems():
+ if name in names:
+ raise ValueError('Repeated name "%s"!' % name)
+
+ test_method = scenario[0]
+ inline_method = inline_method_constructor(test_method, control)
+ event_method = event_method_constructor(test_method, control, pool)
+ adaptation = adapter(test_method)
+
+ methods.append(test_method)
+ inlines[name] = inline_method
+ events[name] = event_method
+ adaptations[name] = adaptation
+ messages[name] = scenario[1]
+
+ return _Assembly(methods, inlines, events, adaptations, messages)
+
+
+def digest(service, control, pool):
+ """Creates a TestServiceDigest from a TestService.
+
+ Args:
+ service: A testing_service.TestService.
+ control: A testing_control.Control.
+ pool: If RPC methods should be serviced in a separate thread, a thread pool.
+ None if RPC methods should be serviced in the thread belonging to the
+ run-time that calls for their service.
+
+ Returns:
+ A TestServiceDigest synthesized from the given service.TestService.
+ """
+ names = set()
+
+ unary_unary = _assemble(
+ service.unary_unary_scenarios(), names, _InlineUnaryUnaryMethod,
+ _EventUnaryUnaryMethod, _UnaryUnaryAdaptation, control, pool)
+ names.update(set(unary_unary.inlines))
+
+ unary_stream = _assemble(
+ service.unary_stream_scenarios(), names, _InlineUnaryStreamMethod,
+ _EventUnaryStreamMethod, _UnaryStreamAdaptation, control, pool)
+ names.update(set(unary_stream.inlines))
+
+ stream_unary = _assemble(
+ service.stream_unary_scenarios(), names, _InlineStreamUnaryMethod,
+ _EventStreamUnaryMethod, _StreamUnaryAdaptation, control, pool)
+ names.update(set(stream_unary.inlines))
+
+ stream_stream = _assemble(
+ service.stream_stream_scenarios(), names, _InlineStreamStreamMethod,
+ _EventStreamStreamMethod, _IDENTITY, control, pool)
+ names.update(set(stream_stream.inlines))
+
+ methods = list(unary_unary.methods)
+ methods.extend(unary_stream.methods)
+ methods.extend(stream_unary.methods)
+ methods.extend(stream_stream.methods)
+ adaptations = dict(unary_unary.adaptations)
+ adaptations.update(unary_stream.adaptations)
+ adaptations.update(stream_unary.adaptations)
+ adaptations.update(stream_stream.adaptations)
+ inlines = dict(unary_unary.inlines)
+ inlines.update(unary_stream.inlines)
+ inlines.update(stream_unary.inlines)
+ inlines.update(stream_stream.inlines)
+ events = dict(unary_unary.events)
+ events.update(unary_stream.events)
+ events.update(stream_unary.events)
+ events.update(stream_stream.events)
+
+ return TestServiceDigest(
+ service.name(),
+ methods,
+ inlines,
+ events,
+ _MultiMethodImplementation(adaptations, control, pool),
+ unary_unary.messages,
+ unary_stream.messages,
+ stream_unary.messages,
+ stream_stream.messages)
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/event_invocation_synchronous_event_service_test_case.py b/src/python/grpcio/tests/unit/framework/face/testing/event_invocation_synchronous_event_service_test_case.py
new file mode 100644
index 0000000000..179f3a2f67
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/event_invocation_synchronous_event_service_test_case.py
@@ -0,0 +1,376 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A test to verify an implementation of the Face layer of RPC Framework."""
+
+import abc
+import unittest
+
+from grpc.framework.face import interfaces
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.face.testing import callback as testing_callback
+from tests.unit.framework.face.testing import control
+from tests.unit.framework.face.testing import coverage
+from tests.unit.framework.face.testing import digest
+from tests.unit.framework.face.testing import stock_service
+from tests.unit.framework.face.testing import test_case
+
+
+class EventInvocationSynchronousEventServiceTestCase(
+ test_case.FaceTestCase, coverage.FullCoverage):
+ """A test of the Face layer of RPC Framework.
+
+ Concrete subclasses must also extend unittest.TestCase.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ def setUp(self):
+ """See unittest.TestCase.setUp for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self.control = control.PauseFailControl()
+ self.digest = digest.digest(
+ stock_service.STOCK_TEST_SERVICE, self.control, None)
+
+ self.stub, self.memo = self.set_up_implementation(
+ self.digest.name, self.digest.methods,
+ self.digest.event_method_implementations, None)
+
+ def tearDown(self):
+ """See unittest.TestCase.tearDown for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self.tear_down_implementation(self.memo)
+
+ def testSuccessfulUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = testing_callback.Callback()
+
+ self.stub.event_value_in_value_out(
+ name, request, callback.complete, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ callback.block_until_terminated()
+ response = callback.response()
+
+ test_messages.verify(request, response, self)
+
+ def testSuccessfulUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = testing_callback.Callback()
+
+ self.stub.event_value_in_stream_out(
+ name, request, callback, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ callback.block_until_terminated()
+ responses = callback.responses()
+
+ test_messages.verify(request, responses, self)
+
+ def testSuccessfulStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = testing_callback.Callback()
+
+ unused_call, request_consumer = self.stub.event_stream_in_value_out(
+ name, callback.complete, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ for request in requests:
+ request_consumer.consume(request)
+ request_consumer.terminate()
+ callback.block_until_terminated()
+ response = callback.response()
+
+ test_messages.verify(requests, response, self)
+
+ def testSuccessfulStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = testing_callback.Callback()
+
+ unused_call, request_consumer = self.stub.event_stream_in_stream_out(
+ name, callback, callback.abort, test_constants.SHORT_TIMEOUT)
+ for request in requests:
+ request_consumer.consume(request)
+ request_consumer.terminate()
+ callback.block_until_terminated()
+ responses = callback.responses()
+
+ test_messages.verify(requests, responses, self)
+
+ def testSequentialInvocations(self):
+ # pylint: disable=cell-var-from-loop
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ second_request = test_messages.request()
+ first_callback = testing_callback.Callback()
+ second_callback = testing_callback.Callback()
+
+ def make_second_invocation(first_response):
+ first_callback.complete(first_response)
+ self.stub.event_value_in_value_out(
+ name, second_request, second_callback.complete,
+ second_callback.abort, test_constants.SHORT_TIMEOUT)
+
+ self.stub.event_value_in_value_out(
+ name, first_request, make_second_invocation, first_callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ second_callback.block_until_terminated()
+
+ first_response = first_callback.response()
+ second_response = second_callback.response()
+ test_messages.verify(first_request, first_response, self)
+ test_messages.verify(second_request, second_response, self)
+
+ def testExpiredUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = testing_callback.Callback()
+
+ with self.control.pause():
+ self.stub.event_value_in_value_out(
+ name, request, callback.complete, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.EXPIRED, callback.abortion())
+
+ def testExpiredUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = testing_callback.Callback()
+
+ with self.control.pause():
+ self.stub.event_value_in_stream_out(
+ name, request, callback, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.EXPIRED, callback.abortion())
+
+ def testExpiredStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for unused_test_messages in test_messages_sequence:
+ callback = testing_callback.Callback()
+
+ self.stub.event_stream_in_value_out(
+ name, callback.complete, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.EXPIRED, callback.abortion())
+
+ def testExpiredStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = testing_callback.Callback()
+
+ unused_call, request_consumer = self.stub.event_stream_in_stream_out(
+ name, callback, callback.abort, test_constants.SHORT_TIMEOUT)
+ for request in requests:
+ request_consumer.consume(request)
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.EXPIRED, callback.abortion())
+
+ def testFailedUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = testing_callback.Callback()
+
+ with self.control.fail():
+ self.stub.event_value_in_value_out(
+ name, request, callback.complete, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.SERVICER_FAILURE,
+ callback.abortion())
+
+ def testFailedUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = testing_callback.Callback()
+
+ with self.control.fail():
+ self.stub.event_value_in_stream_out(
+ name, request, callback, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.SERVICER_FAILURE,
+ callback.abortion())
+
+ def testFailedStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = testing_callback.Callback()
+
+ with self.control.fail():
+ unused_call, request_consumer = self.stub.event_stream_in_value_out(
+ name, callback.complete, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ for request in requests:
+ request_consumer.consume(request)
+ request_consumer.terminate()
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.SERVICER_FAILURE,
+ callback.abortion())
+
+ def testFailedStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = testing_callback.Callback()
+
+ with self.control.fail():
+ unused_call, request_consumer = self.stub.event_stream_in_stream_out(
+ name, callback, callback.abort, test_constants.SHORT_TIMEOUT)
+ for request in requests:
+ request_consumer.consume(request)
+ request_consumer.terminate()
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.SERVICER_FAILURE, callback.abortion())
+
+ def testParallelInvocations(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ first_callback = testing_callback.Callback()
+ second_request = test_messages.request()
+ second_callback = testing_callback.Callback()
+
+ self.stub.event_value_in_value_out(
+ name, first_request, first_callback.complete, first_callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ self.stub.event_value_in_value_out(
+ name, second_request, second_callback.complete,
+ second_callback.abort, test_constants.SHORT_TIMEOUT)
+ first_callback.block_until_terminated()
+ second_callback.block_until_terminated()
+
+ first_response = first_callback.response()
+ second_response = second_callback.response()
+ test_messages.verify(first_request, first_response, self)
+ test_messages.verify(second_request, second_response, self)
+
+ @unittest.skip('TODO(nathaniel): implement.')
+ def testWaitingForSomeButNotAllParallelInvocations(self):
+ raise NotImplementedError()
+
+ def testCancelledUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = testing_callback.Callback()
+
+ with self.control.pause():
+ call = self.stub.event_value_in_value_out(
+ name, request, callback.complete, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ call.cancel()
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.CANCELLED, callback.abortion())
+
+ def testCancelledUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = testing_callback.Callback()
+
+ call = self.stub.event_value_in_stream_out(
+ name, request, callback, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ call.cancel()
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.CANCELLED, callback.abortion())
+
+ def testCancelledStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = testing_callback.Callback()
+
+ call, request_consumer = self.stub.event_stream_in_value_out(
+ name, callback.complete, callback.abort,
+ test_constants.SHORT_TIMEOUT)
+ for request in requests:
+ request_consumer.consume(request)
+ call.cancel()
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.CANCELLED, callback.abortion())
+
+ def testCancelledStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for unused_test_messages in test_messages_sequence:
+ callback = testing_callback.Callback()
+
+ call, unused_request_consumer = self.stub.event_stream_in_stream_out(
+ name, callback, callback.abort, test_constants.SHORT_TIMEOUT)
+ call.cancel()
+ callback.block_until_terminated()
+
+ self.assertEqual(interfaces.Abortion.CANCELLED, callback.abortion())
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/future_invocation_asynchronous_event_service_test_case.py b/src/python/grpcio/tests/unit/framework/face/testing/future_invocation_asynchronous_event_service_test_case.py
new file mode 100644
index 0000000000..485524a356
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/future_invocation_asynchronous_event_service_test_case.py
@@ -0,0 +1,379 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A test to verify an implementation of the Face layer of RPC Framework."""
+
+import abc
+import contextlib
+import threading
+import unittest
+
+from grpc.framework.face import exceptions
+from grpc.framework.foundation import future
+from grpc.framework.foundation import logging_pool
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.face.testing import control
+from tests.unit.framework.face.testing import coverage
+from tests.unit.framework.face.testing import digest
+from tests.unit.framework.face.testing import stock_service
+from tests.unit.framework.face.testing import test_case
+
+_MAXIMUM_POOL_SIZE = 10
+
+
+class _PauseableIterator(object):
+
+ def __init__(self, upstream):
+ self._upstream = upstream
+ self._condition = threading.Condition()
+ self._paused = False
+
+ @contextlib.contextmanager
+ def pause(self):
+ with self._condition:
+ self._paused = True
+ yield
+ with self._condition:
+ self._paused = False
+ self._condition.notify_all()
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ with self._condition:
+ while self._paused:
+ self._condition.wait()
+ return next(self._upstream)
+
+
+class FutureInvocationAsynchronousEventServiceTestCase(
+ test_case.FaceTestCase, coverage.FullCoverage):
+ """A test of the Face layer of RPC Framework.
+
+ Concrete subclasses must also extend unittest.TestCase.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ def setUp(self):
+ """See unittest.TestCase.setUp for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self.control = control.PauseFailControl()
+ self.digest_pool = logging_pool.pool(_MAXIMUM_POOL_SIZE)
+ self.digest = digest.digest(
+ stock_service.STOCK_TEST_SERVICE, self.control, self.digest_pool)
+
+ self.stub, self.memo = self.set_up_implementation(
+ self.digest.name, self.digest.methods,
+ self.digest.event_method_implementations, None)
+
+ def tearDown(self):
+ """See unittest.TestCase.tearDown for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self.tear_down_implementation(self.memo)
+ self.digest_pool.shutdown(wait=True)
+
+ def testSuccessfulUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ response_future = self.stub.future_value_in_value_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+ response = response_future.result()
+
+ test_messages.verify(request, response, self)
+
+ def testSuccessfulUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ response_iterator = self.stub.inline_value_in_stream_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+ responses = list(response_iterator)
+
+ test_messages.verify(request, responses, self)
+
+ def testSuccessfulStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ request_iterator = _PauseableIterator(iter(requests))
+
+ # Use of a paused iterator of requests allows us to test that control is
+ # returned to calling code before the iterator yields any requests.
+ with request_iterator.pause():
+ response_future = self.stub.future_stream_in_value_out(
+ name, request_iterator, test_constants.SHORT_TIMEOUT)
+ response = response_future.result()
+
+ test_messages.verify(requests, response, self)
+
+ def testSuccessfulStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ request_iterator = _PauseableIterator(iter(requests))
+
+ # Use of a paused iterator of requests allows us to test that control is
+ # returned to calling code before the iterator yields any requests.
+ with request_iterator.pause():
+ response_iterator = self.stub.inline_stream_in_stream_out(
+ name, request_iterator, test_constants.SHORT_TIMEOUT)
+ responses = list(response_iterator)
+
+ test_messages.verify(requests, responses, self)
+
+ def testSequentialInvocations(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ second_request = test_messages.request()
+
+ first_response_future = self.stub.future_value_in_value_out(
+ name, first_request, test_constants.SHORT_TIMEOUT)
+ first_response = first_response_future.result()
+
+ test_messages.verify(first_request, first_response, self)
+
+ second_response_future = self.stub.future_value_in_value_out(
+ name, second_request, test_constants.SHORT_TIMEOUT)
+ second_response = second_response_future.result()
+
+ test_messages.verify(second_request, second_response, self)
+
+ def testExpiredUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.pause():
+ multi_callable = self.stub.unary_unary_multi_callable(name)
+ response_future = multi_callable.future(request,
+ test_constants.SHORT_TIMEOUT)
+ self.assertIsInstance(
+ response_future.exception(), exceptions.ExpirationError)
+ with self.assertRaises(exceptions.ExpirationError):
+ response_future.result()
+
+ def testExpiredUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.pause():
+ response_iterator = self.stub.inline_value_in_stream_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+ with self.assertRaises(exceptions.ExpirationError):
+ list(response_iterator)
+
+ def testExpiredStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.pause():
+ multi_callable = self.stub.stream_unary_multi_callable(name)
+ response_future = multi_callable.future(iter(requests),
+ test_constants.SHORT_TIMEOUT)
+ self.assertIsInstance(
+ response_future.exception(), exceptions.ExpirationError)
+ with self.assertRaises(exceptions.ExpirationError):
+ response_future.result()
+
+ def testExpiredStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.pause():
+ response_iterator = self.stub.inline_stream_in_stream_out(
+ name, iter(requests), test_constants.SHORT_TIMEOUT)
+ with self.assertRaises(exceptions.ExpirationError):
+ list(response_iterator)
+
+ def testFailedUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.fail():
+ response_future = self.stub.future_value_in_value_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+
+ # Because the servicer fails outside of the thread from which the
+ # servicer-side runtime called into it its failure is
+ # indistinguishable from simply not having called its
+ # response_callback before the expiration of the RPC.
+ self.assertIsInstance(
+ response_future.exception(), exceptions.ExpirationError)
+ with self.assertRaises(exceptions.ExpirationError):
+ response_future.result()
+
+ def testFailedUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ # Because the servicer fails outside of the thread from which the
+ # servicer-side runtime called into it its failure is indistinguishable
+ # from simply not having called its response_consumer before the
+ # expiration of the RPC.
+ with self.control.fail(), self.assertRaises(exceptions.ExpirationError):
+ response_iterator = self.stub.inline_value_in_stream_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+ list(response_iterator)
+
+ def testFailedStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.fail():
+ response_future = self.stub.future_stream_in_value_out(
+ name, iter(requests), test_constants.SHORT_TIMEOUT)
+
+ # Because the servicer fails outside of the thread from which the
+ # servicer-side runtime called into it its failure is
+ # indistinguishable from simply not having called its
+ # response_callback before the expiration of the RPC.
+ self.assertIsInstance(
+ response_future.exception(), exceptions.ExpirationError)
+ with self.assertRaises(exceptions.ExpirationError):
+ response_future.result()
+
+ def testFailedStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ # Because the servicer fails outside of the thread from which the
+ # servicer-side runtime called into it its failure is indistinguishable
+ # from simply not having called its response_consumer before the
+ # expiration of the RPC.
+ with self.control.fail(), self.assertRaises(exceptions.ExpirationError):
+ response_iterator = self.stub.inline_stream_in_stream_out(
+ name, iter(requests), test_constants.SHORT_TIMEOUT)
+ list(response_iterator)
+
+ def testParallelInvocations(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ second_request = test_messages.request()
+
+ # TODO(bug 2039): use LONG_TIMEOUT instead
+ first_response_future = self.stub.future_value_in_value_out(
+ name, first_request, test_constants.SHORT_TIMEOUT)
+ second_response_future = self.stub.future_value_in_value_out(
+ name, second_request, test_constants.SHORT_TIMEOUT)
+ first_response = first_response_future.result()
+ second_response = second_response_future.result()
+
+ test_messages.verify(first_request, first_response, self)
+ test_messages.verify(second_request, second_response, self)
+
+ @unittest.skip('TODO(nathaniel): implement.')
+ def testWaitingForSomeButNotAllParallelInvocations(self):
+ raise NotImplementedError()
+
+ def testCancelledUnaryRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.pause():
+ response_future = self.stub.future_value_in_value_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+ cancel_method_return_value = response_future.cancel()
+
+ self.assertFalse(cancel_method_return_value)
+ self.assertTrue(response_future.cancelled())
+
+ def testCancelledUnaryRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self.control.pause():
+ response_iterator = self.stub.inline_value_in_stream_out(
+ name, request, test_constants.SHORT_TIMEOUT)
+ response_iterator.cancel()
+
+ with self.assertRaises(future.CancelledError):
+ next(response_iterator)
+
+ def testCancelledStreamRequestUnaryResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.pause():
+ response_future = self.stub.future_stream_in_value_out(
+ name, iter(requests), test_constants.SHORT_TIMEOUT)
+ cancel_method_return_value = response_future.cancel()
+
+ self.assertFalse(cancel_method_return_value)
+ self.assertTrue(response_future.cancelled())
+
+ def testCancelledStreamRequestStreamResponse(self):
+ for name, test_messages_sequence in (
+ self.digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self.control.pause():
+ response_iterator = self.stub.inline_stream_in_stream_out(
+ name, iter(requests), test_constants.SHORT_TIMEOUT)
+ response_iterator.cancel()
+
+ with self.assertRaises(future.CancelledError):
+ next(response_iterator)
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/interfaces.py b/src/python/grpcio/tests/unit/framework/face/testing/interfaces.py
new file mode 100644
index 0000000000..5932dabf1e
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/interfaces.py
@@ -0,0 +1,117 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Interfaces implemented by data sets used in Face-layer tests."""
+
+import abc
+
+# cardinality is referenced from specification in this module.
+from grpc.framework.common import cardinality # pylint: disable=unused-import
+
+
+class Method(object):
+ """An RPC method to be used in tests of RPC implementations."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def name(self):
+ """Identify the name of the method.
+
+ Returns:
+ The name of the method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def cardinality(self):
+ """Identify the cardinality of the method.
+
+ Returns:
+ A cardinality.Cardinality value describing the streaming semantics of the
+ method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def request_class(self):
+ """Identify the class used for the method's request objects.
+
+ Returns:
+ The class object of the class to which the method's request objects
+ belong.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def response_class(self):
+ """Identify the class used for the method's response objects.
+
+ Returns:
+ The class object of the class to which the method's response objects
+ belong.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_request(self, request):
+ """Serialize the given request object.
+
+ Args:
+ request: A request object appropriate for this method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_request(self, serialized_request):
+ """Synthesize a request object from a given bytestring.
+
+ Args:
+ serialized_request: A bytestring deserializable into a request object
+ appropriate for this method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_response(self, response):
+ """Serialize the given response object.
+
+ Args:
+ response: A response object appropriate for this method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_response(self, serialized_response):
+ """Synthesize a response object from a given bytestring.
+
+ Args:
+ serialized_response: A bytestring deserializable into a response object
+ appropriate for this method.
+ """
+ raise NotImplementedError()
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/service.py b/src/python/grpcio/tests/unit/framework/face/testing/service.py
new file mode 100644
index 0000000000..ac0b89b6ee
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/service.py
@@ -0,0 +1,337 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Private interfaces implemented by data sets used in Face-layer tests."""
+
+import abc
+
+# interfaces is referenced from specification in this module.
+from grpc.framework.face import interfaces as face_interfaces # pylint: disable=unused-import
+from tests.unit.framework.face.testing import interfaces
+
+
+class UnaryUnaryTestMethodImplementation(interfaces.Method):
+ """A controllable implementation of a unary-unary RPC method."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def service(self, request, response_callback, context, control):
+ """Services an RPC that accepts one message and produces one message.
+
+ Args:
+ request: The single request message for the RPC.
+ response_callback: A callback to be called to accept the response message
+ of the RPC.
+ context: An face_interfaces.RpcContext object.
+ control: A test_control.Control to control execution of this method.
+
+ Raises:
+ abandonment.Abandoned: May or may not be raised when the RPC has been
+ aborted.
+ """
+ raise NotImplementedError()
+
+
+class UnaryUnaryTestMessages(object):
+ """A type for unary-request-unary-response message pairings."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def request(self):
+ """Affords a request message.
+
+ Implementations of this method should return a different message with each
+ call so that multiple test executions of the test method may be made with
+ different inputs.
+
+ Returns:
+ A request message.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify(self, request, response, test_case):
+ """Verifies that the computed response matches the given request.
+
+ Args:
+ request: A request message.
+ response: A response message.
+ test_case: A unittest.TestCase object affording useful assertion methods.
+
+ Raises:
+ AssertionError: If the request and response do not match, indicating that
+ there was some problem executing the RPC under test.
+ """
+ raise NotImplementedError()
+
+
+class UnaryStreamTestMethodImplementation(interfaces.Method):
+ """A controllable implementation of a unary-stream RPC method."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def service(self, request, response_consumer, context, control):
+ """Services an RPC that takes one message and produces a stream of messages.
+
+ Args:
+ request: The single request message for the RPC.
+ response_consumer: A stream.Consumer to be called to accept the response
+ messages of the RPC.
+ context: A face_interfaces.RpcContext object.
+ control: A test_control.Control to control execution of this method.
+
+ Raises:
+ abandonment.Abandoned: May or may not be raised when the RPC has been
+ aborted.
+ """
+ raise NotImplementedError()
+
+
+class UnaryStreamTestMessages(object):
+ """A type for unary-request-stream-response message pairings."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def request(self):
+ """Affords a request message.
+
+ Implementations of this method should return a different message with each
+ call so that multiple test executions of the test method may be made with
+ different inputs.
+
+ Returns:
+ A request message.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify(self, request, responses, test_case):
+ """Verifies that the computed responses match the given request.
+
+ Args:
+ request: A request message.
+ responses: A sequence of response messages.
+ test_case: A unittest.TestCase object affording useful assertion methods.
+
+ Raises:
+ AssertionError: If the request and responses do not match, indicating that
+ there was some problem executing the RPC under test.
+ """
+ raise NotImplementedError()
+
+
+class StreamUnaryTestMethodImplementation(interfaces.Method):
+ """A controllable implementation of a stream-unary RPC method."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def service(self, response_callback, context, control):
+ """Services an RPC that takes a stream of messages and produces one message.
+
+ Args:
+ response_callback: A callback to be called to accept the response message
+ of the RPC.
+ context: A face_interfaces.RpcContext object.
+ control: A test_control.Control to control execution of this method.
+
+ Returns:
+ A stream.Consumer with which to accept the request messages of the RPC.
+ The consumer returned from this method may or may not be invoked to
+ completion: in the case of RPC abortion, RPC Framework will simply stop
+ passing messages to this object. Implementations must not assume that
+ this object will be called to completion of the request stream or even
+ called at all.
+
+ Raises:
+ abandonment.Abandoned: May or may not be raised when the RPC has been
+ aborted.
+ """
+ raise NotImplementedError()
+
+
+class StreamUnaryTestMessages(object):
+ """A type for stream-request-unary-response message pairings."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def requests(self):
+ """Affords a sequence of request messages.
+
+ Implementations of this method should return a different sequences with each
+ call so that multiple test executions of the test method may be made with
+ different inputs.
+
+ Returns:
+ A sequence of request messages.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify(self, requests, response, test_case):
+ """Verifies that the computed response matches the given requests.
+
+ Args:
+ requests: A sequence of request messages.
+ response: A response message.
+ test_case: A unittest.TestCase object affording useful assertion methods.
+
+ Raises:
+ AssertionError: If the requests and response do not match, indicating that
+ there was some problem executing the RPC under test.
+ """
+ raise NotImplementedError()
+
+
+class StreamStreamTestMethodImplementation(interfaces.Method):
+ """A controllable implementation of a stream-stream RPC method."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def service(self, response_consumer, context, control):
+ """Services an RPC that accepts and produces streams of messages.
+
+ Args:
+ response_consumer: A stream.Consumer to be called to accept the response
+ messages of the RPC.
+ context: A face_interfaces.RpcContext object.
+ control: A test_control.Control to control execution of this method.
+
+ Returns:
+ A stream.Consumer with which to accept the request messages of the RPC.
+ The consumer returned from this method may or may not be invoked to
+ completion: in the case of RPC abortion, RPC Framework will simply stop
+ passing messages to this object. Implementations must not assume that
+ this object will be called to completion of the request stream or even
+ called at all.
+
+ Raises:
+ abandonment.Abandoned: May or may not be raised when the RPC has been
+ aborted.
+ """
+ raise NotImplementedError()
+
+
+class StreamStreamTestMessages(object):
+ """A type for stream-request-stream-response message pairings."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def requests(self):
+ """Affords a sequence of request messages.
+
+ Implementations of this method should return a different sequences with each
+ call so that multiple test executions of the test method may be made with
+ different inputs.
+
+ Returns:
+ A sequence of request messages.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify(self, requests, responses, test_case):
+ """Verifies that the computed response matches the given requests.
+
+ Args:
+ requests: A sequence of request messages.
+ responses: A sequence of response messages.
+ test_case: A unittest.TestCase object affording useful assertion methods.
+
+ Raises:
+ AssertionError: If the requests and responses do not match, indicating
+ that there was some problem executing the RPC under test.
+ """
+ raise NotImplementedError()
+
+
+class TestService(object):
+ """A specification of implemented RPC methods to use in tests."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def name(self):
+ """Identifies the RPC service name used during the test.
+
+ Returns:
+ The RPC service name to be used for the test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def unary_unary_scenarios(self):
+ """Affords unary-request-unary-response test methods and their messages.
+
+ Returns:
+ A dict from method name to pair. The first element of the pair
+ is a UnaryUnaryTestMethodImplementation object and the second element
+ is a sequence of UnaryUnaryTestMethodMessages objects.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def unary_stream_scenarios(self):
+ """Affords unary-request-stream-response test methods and their messages.
+
+ Returns:
+ A dict from method name to pair. The first element of the pair is a
+ UnaryStreamTestMethodImplementation object and the second element is a
+ sequence of UnaryStreamTestMethodMessages objects.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def stream_unary_scenarios(self):
+ """Affords stream-request-unary-response test methods and their messages.
+
+ Returns:
+ A dict from method name to pair. The first element of the pair is a
+ StreamUnaryTestMethodImplementation object and the second element is a
+ sequence of StreamUnaryTestMethodMessages objects.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def stream_stream_scenarios(self):
+ """Affords stream-request-stream-response test methods and their messages.
+
+ Returns:
+ A dict from method name to pair. The first element of the pair is a
+ StreamStreamTestMethodImplementation object and the second element is a
+ sequence of StreamStreamTestMethodMessages objects.
+ """
+ raise NotImplementedError()
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/stock_service.py b/src/python/grpcio/tests/unit/framework/face/testing/stock_service.py
new file mode 100644
index 0000000000..117c723f79
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/stock_service.py
@@ -0,0 +1,374 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Examples of Python implementations of the stock.proto Stock service."""
+
+from grpc.framework.common import cardinality
+from grpc.framework.foundation import abandonment
+from grpc.framework.foundation import stream
+from grpc.framework.foundation import stream_util
+from tests.unit.framework.face.testing import service
+from tests.unit._junkdrawer import stock_pb2
+
+SYMBOL_FORMAT = 'test symbol:%03d'
+STREAM_LENGTH = 400
+
+# A test-appropriate security-pricing function. :-P
+_price = lambda symbol_name: float(hash(symbol_name) % 4096)
+
+
+def _get_last_trade_price(stock_request, stock_reply_callback, control, active):
+ """A unary-request, unary-response test method."""
+ control.control()
+ if active():
+ stock_reply_callback(
+ stock_pb2.StockReply(
+ symbol=stock_request.symbol, price=_price(stock_request.symbol)))
+ else:
+ raise abandonment.Abandoned()
+
+
+def _get_last_trade_price_multiple(stock_reply_consumer, control, active):
+ """A stream-request, stream-response test method."""
+ def stock_reply_for_stock_request(stock_request):
+ control.control()
+ if active():
+ return stock_pb2.StockReply(
+ symbol=stock_request.symbol, price=_price(stock_request.symbol))
+ else:
+ raise abandonment.Abandoned()
+ return stream_util.TransformingConsumer(
+ stock_reply_for_stock_request, stock_reply_consumer)
+
+
+def _watch_future_trades(stock_request, stock_reply_consumer, control, active):
+ """A unary-request, stream-response test method."""
+ base_price = _price(stock_request.symbol)
+ for index in range(stock_request.num_trades_to_watch):
+ control.control()
+ if active():
+ stock_reply_consumer.consume(
+ stock_pb2.StockReply(
+ symbol=stock_request.symbol, price=base_price + index))
+ else:
+ raise abandonment.Abandoned()
+ stock_reply_consumer.terminate()
+
+
+def _get_highest_trade_price(stock_reply_callback, control, active):
+ """A stream-request, unary-response test method."""
+
+ class StockRequestConsumer(stream.Consumer):
+ """Keeps an ongoing record of the most valuable symbol yet consumed."""
+
+ def __init__(self):
+ self._symbol = None
+ self._price = None
+
+ def consume(self, stock_request):
+ control.control()
+ if active():
+ if self._price is None:
+ self._symbol = stock_request.symbol
+ self._price = _price(stock_request.symbol)
+ else:
+ candidate_price = _price(stock_request.symbol)
+ if self._price < candidate_price:
+ self._symbol = stock_request.symbol
+ self._price = candidate_price
+
+ def terminate(self):
+ control.control()
+ if active():
+ if self._symbol is None:
+ raise ValueError()
+ else:
+ stock_reply_callback(
+ stock_pb2.StockReply(symbol=self._symbol, price=self._price))
+ self._symbol = None
+ self._price = None
+
+ def consume_and_terminate(self, stock_request):
+ control.control()
+ if active():
+ if self._price is None:
+ stock_reply_callback(
+ stock_pb2.StockReply(
+ symbol=stock_request.symbol,
+ price=_price(stock_request.symbol)))
+ else:
+ candidate_price = _price(stock_request.symbol)
+ if self._price < candidate_price:
+ stock_reply_callback(
+ stock_pb2.StockReply(
+ symbol=stock_request.symbol, price=candidate_price))
+ else:
+ stock_reply_callback(
+ stock_pb2.StockReply(
+ symbol=self._symbol, price=self._price))
+
+ self._symbol = None
+ self._price = None
+
+ return StockRequestConsumer()
+
+
+class GetLastTradePrice(service.UnaryUnaryTestMethodImplementation):
+ """GetLastTradePrice for use in tests."""
+
+ def name(self):
+ return 'GetLastTradePrice'
+
+ def cardinality(self):
+ return cardinality.Cardinality.UNARY_UNARY
+
+ def request_class(self):
+ return stock_pb2.StockRequest
+
+ def response_class(self):
+ return stock_pb2.StockReply
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, serialized_request):
+ return stock_pb2.StockRequest.FromString(serialized_request)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, serialized_response):
+ return stock_pb2.StockReply.FromString(serialized_response)
+
+ def service(self, request, response_callback, context, control):
+ _get_last_trade_price(
+ request, response_callback, control, context.is_active)
+
+
+class GetLastTradePriceMessages(service.UnaryUnaryTestMessages):
+
+ def __init__(self):
+ self._index = 0
+
+ def request(self):
+ symbol = SYMBOL_FORMAT % self._index
+ self._index += 1
+ return stock_pb2.StockRequest(symbol=symbol)
+
+ def verify(self, request, response, test_case):
+ test_case.assertEqual(request.symbol, response.symbol)
+ test_case.assertEqual(_price(request.symbol), response.price)
+
+
+class GetLastTradePriceMultiple(service.StreamStreamTestMethodImplementation):
+ """GetLastTradePriceMultiple for use in tests."""
+
+ def name(self):
+ return 'GetLastTradePriceMultiple'
+
+ def cardinality(self):
+ return cardinality.Cardinality.STREAM_STREAM
+
+ def request_class(self):
+ return stock_pb2.StockRequest
+
+ def response_class(self):
+ return stock_pb2.StockReply
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, serialized_request):
+ return stock_pb2.StockRequest.FromString(serialized_request)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, serialized_response):
+ return stock_pb2.StockReply.FromString(serialized_response)
+
+ def service(self, response_consumer, context, control):
+ return _get_last_trade_price_multiple(
+ response_consumer, control, context.is_active)
+
+
+class GetLastTradePriceMultipleMessages(service.StreamStreamTestMessages):
+ """Pairs of message streams for use with GetLastTradePriceMultiple."""
+
+ def __init__(self):
+ self._index = 0
+
+ def requests(self):
+ base_index = self._index
+ self._index += 1
+ return [
+ stock_pb2.StockRequest(symbol=SYMBOL_FORMAT % (base_index + index))
+ for index in range(STREAM_LENGTH)]
+
+ def verify(self, requests, responses, test_case):
+ test_case.assertEqual(len(requests), len(responses))
+ for stock_request, stock_reply in zip(requests, responses):
+ test_case.assertEqual(stock_request.symbol, stock_reply.symbol)
+ test_case.assertEqual(_price(stock_request.symbol), stock_reply.price)
+
+
+class WatchFutureTrades(service.UnaryStreamTestMethodImplementation):
+ """WatchFutureTrades for use in tests."""
+
+ def name(self):
+ return 'WatchFutureTrades'
+
+ def cardinality(self):
+ return cardinality.Cardinality.UNARY_STREAM
+
+ def request_class(self):
+ return stock_pb2.StockRequest
+
+ def response_class(self):
+ return stock_pb2.StockReply
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, serialized_request):
+ return stock_pb2.StockRequest.FromString(serialized_request)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, serialized_response):
+ return stock_pb2.StockReply.FromString(serialized_response)
+
+ def service(self, request, response_consumer, context, control):
+ _watch_future_trades(request, response_consumer, control, context.is_active)
+
+
+class WatchFutureTradesMessages(service.UnaryStreamTestMessages):
+ """Pairs of a single request message and a sequence of response messages."""
+
+ def __init__(self):
+ self._index = 0
+
+ def request(self):
+ symbol = SYMBOL_FORMAT % self._index
+ self._index += 1
+ return stock_pb2.StockRequest(
+ symbol=symbol, num_trades_to_watch=STREAM_LENGTH)
+
+ def verify(self, request, responses, test_case):
+ test_case.assertEqual(STREAM_LENGTH, len(responses))
+ base_price = _price(request.symbol)
+ for index, response in enumerate(responses):
+ test_case.assertEqual(base_price + index, response.price)
+
+
+class GetHighestTradePrice(service.StreamUnaryTestMethodImplementation):
+ """GetHighestTradePrice for use in tests."""
+
+ def name(self):
+ return 'GetHighestTradePrice'
+
+ def cardinality(self):
+ return cardinality.Cardinality.STREAM_UNARY
+
+ def request_class(self):
+ return stock_pb2.StockRequest
+
+ def response_class(self):
+ return stock_pb2.StockReply
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, serialized_request):
+ return stock_pb2.StockRequest.FromString(serialized_request)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, serialized_response):
+ return stock_pb2.StockReply.FromString(serialized_response)
+
+ def service(self, response_callback, context, control):
+ return _get_highest_trade_price(
+ response_callback, control, context.is_active)
+
+
+class GetHighestTradePriceMessages(service.StreamUnaryTestMessages):
+
+ def requests(self):
+ return [
+ stock_pb2.StockRequest(symbol=SYMBOL_FORMAT % index)
+ for index in range(STREAM_LENGTH)]
+
+ def verify(self, requests, response, test_case):
+ price = None
+ symbol = None
+ for stock_request in requests:
+ current_symbol = stock_request.symbol
+ current_price = _price(current_symbol)
+ if price is None or price < current_price:
+ price = current_price
+ symbol = current_symbol
+ test_case.assertEqual(price, response.price)
+ test_case.assertEqual(symbol, response.symbol)
+
+
+class StockTestService(service.TestService):
+ """A corpus of test data with one method of each RPC cardinality."""
+
+ def name(self):
+ return 'Stock'
+
+ def unary_unary_scenarios(self):
+ return {
+ 'GetLastTradePrice': (
+ GetLastTradePrice(), [GetLastTradePriceMessages()]),
+ }
+
+ def unary_stream_scenarios(self):
+ return {
+ 'WatchFutureTrades': (
+ WatchFutureTrades(), [WatchFutureTradesMessages()]),
+ }
+
+ def stream_unary_scenarios(self):
+ return {
+ 'GetHighestTradePrice': (
+ GetHighestTradePrice(), [GetHighestTradePriceMessages()])
+ }
+
+ def stream_stream_scenarios(self):
+ return {
+ 'GetLastTradePriceMultiple': (
+ GetLastTradePriceMultiple(), [GetLastTradePriceMultipleMessages()]),
+ }
+
+
+STOCK_TEST_SERVICE = StockTestService()
diff --git a/src/python/grpcio/tests/unit/framework/face/testing/test_case.py b/src/python/grpcio/tests/unit/framework/face/testing/test_case.py
new file mode 100644
index 0000000000..23d4d919c2
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/face/testing/test_case.py
@@ -0,0 +1,80 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tools for creating tests of implementations of the Face layer."""
+
+import abc
+
+# face_interfaces and interfaces are referenced in specification in this module.
+from grpc.framework.face import interfaces as face_interfaces # pylint: disable=unused-import
+from tests.unit.framework.face.testing import interfaces # pylint: disable=unused-import
+
+
+class FaceTestCase(object):
+ """Describes a test of the Face Layer of RPC Framework.
+
+ Concrete subclasses must also inherit from unittest.TestCase and from at least
+ one class that defines test methods.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def set_up_implementation(
+ self, name, methods, method_implementations,
+ multi_method_implementation):
+ """Instantiates the Face Layer implementation under test.
+
+ Args:
+ name: The service name to be used in the test.
+ methods: A sequence of interfaces.Method objects describing the RPC
+ methods that will be called during the test.
+ method_implementations: A dictionary from string RPC method name to
+ face_interfaces.MethodImplementation object specifying
+ implementation of an RPC method.
+ multi_method_implementation: An face_interfaces.MultiMethodImplementation
+ or None.
+
+ Returns:
+ A sequence of length two the first element of which is a
+ face_interfaces.GenericStub (backed by the given method
+ implementations), and the second element of which is an arbitrary memo
+ object to be kept and passed to tearDownImplementation at the conclusion
+ of the test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def tear_down_implementation(self, memo):
+ """Destroys the Face layer implementation under test.
+
+ Args:
+ memo: The object from the second position of the return value of
+ set_up_implementation.
+ """
+ raise NotImplementedError()
diff --git a/src/python/grpcio/tests/unit/framework/foundation/__init__.py b/src/python/grpcio/tests/unit/framework/foundation/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/foundation/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/foundation/_later_test.py b/src/python/grpcio/tests/unit/framework/foundation/_later_test.py
new file mode 100644
index 0000000000..6c2459e185
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/foundation/_later_test.py
@@ -0,0 +1,151 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests of the later module."""
+
+import threading
+import time
+import unittest
+
+from grpc.framework.foundation import later
+
+TICK = 0.1
+
+
+class LaterTest(unittest.TestCase):
+
+ def test_simple_delay(self):
+ lock = threading.Lock()
+ cell = [0]
+ return_value = object()
+
+ def computation():
+ with lock:
+ cell[0] += 1
+ return return_value
+ computation_future = later.later(TICK * 2, computation)
+
+ self.assertFalse(computation_future.done())
+ self.assertFalse(computation_future.cancelled())
+ time.sleep(TICK)
+ self.assertFalse(computation_future.done())
+ self.assertFalse(computation_future.cancelled())
+ with lock:
+ self.assertEqual(0, cell[0])
+ time.sleep(TICK * 2)
+ self.assertTrue(computation_future.done())
+ self.assertFalse(computation_future.cancelled())
+ with lock:
+ self.assertEqual(1, cell[0])
+ self.assertEqual(return_value, computation_future.result())
+
+ def test_callback(self):
+ lock = threading.Lock()
+ cell = [0]
+ callback_called = [False]
+ future_passed_to_callback = [None]
+ def computation():
+ with lock:
+ cell[0] += 1
+ computation_future = later.later(TICK * 2, computation)
+ def callback(outcome):
+ with lock:
+ callback_called[0] = True
+ future_passed_to_callback[0] = outcome
+ computation_future.add_done_callback(callback)
+ time.sleep(TICK)
+ with lock:
+ self.assertFalse(callback_called[0])
+ time.sleep(TICK * 2)
+ with lock:
+ self.assertTrue(callback_called[0])
+ self.assertTrue(future_passed_to_callback[0].done())
+
+ callback_called[0] = False
+ future_passed_to_callback[0] = None
+
+ computation_future.add_done_callback(callback)
+ with lock:
+ self.assertTrue(callback_called[0])
+ self.assertTrue(future_passed_to_callback[0].done())
+
+ def test_cancel(self):
+ lock = threading.Lock()
+ cell = [0]
+ callback_called = [False]
+ future_passed_to_callback = [None]
+ def computation():
+ with lock:
+ cell[0] += 1
+ computation_future = later.later(TICK * 2, computation)
+ def callback(outcome):
+ with lock:
+ callback_called[0] = True
+ future_passed_to_callback[0] = outcome
+ computation_future.add_done_callback(callback)
+ time.sleep(TICK)
+ with lock:
+ self.assertFalse(callback_called[0])
+ computation_future.cancel()
+ self.assertTrue(computation_future.cancelled())
+ self.assertFalse(computation_future.running())
+ self.assertTrue(computation_future.done())
+ with lock:
+ self.assertTrue(callback_called[0])
+ self.assertTrue(future_passed_to_callback[0].cancelled())
+
+ def test_result(self):
+ lock = threading.Lock()
+ cell = [0]
+ callback_called = [False]
+ future_passed_to_callback_cell = [None]
+ return_value = object()
+
+ def computation():
+ with lock:
+ cell[0] += 1
+ return return_value
+ computation_future = later.later(TICK * 2, computation)
+
+ def callback(future_passed_to_callback):
+ with lock:
+ callback_called[0] = True
+ future_passed_to_callback_cell[0] = future_passed_to_callback
+ computation_future.add_done_callback(callback)
+ returned_value = computation_future.result()
+ self.assertEqual(return_value, returned_value)
+
+ # The callback may not yet have been called! Sleep a tick.
+ time.sleep(TICK)
+ with lock:
+ self.assertTrue(callback_called[0])
+ self.assertEqual(return_value, future_passed_to_callback_cell[0].result())
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/framework/foundation/_logging_pool_test.py b/src/python/grpcio/tests/unit/framework/foundation/_logging_pool_test.py
new file mode 100644
index 0000000000..452802da6a
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/foundation/_logging_pool_test.py
@@ -0,0 +1,64 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests for grpc.framework.foundation.logging_pool."""
+
+import unittest
+
+from grpc.framework.foundation import logging_pool
+
+_POOL_SIZE = 16
+
+
+class LoggingPoolTest(unittest.TestCase):
+
+ def testUpAndDown(self):
+ pool = logging_pool.pool(_POOL_SIZE)
+ pool.shutdown(wait=True)
+
+ with logging_pool.pool(_POOL_SIZE) as pool:
+ self.assertIsNotNone(pool)
+
+ def testTaskExecuted(self):
+ test_list = []
+
+ with logging_pool.pool(_POOL_SIZE) as pool:
+ pool.submit(lambda: test_list.append(object())).result()
+
+ self.assertTrue(test_list)
+
+ def testException(self):
+ with logging_pool.pool(_POOL_SIZE) as pool:
+ raised_exception = pool.submit(lambda: 1/0).exception()
+
+ self.assertIsNotNone(raised_exception)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/python/grpcio/tests/unit/framework/foundation/stream_testing.py b/src/python/grpcio/tests/unit/framework/foundation/stream_testing.py
new file mode 100644
index 0000000000..098a53d5e7
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/foundation/stream_testing.py
@@ -0,0 +1,73 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Utilities for testing stream-related code."""
+
+from grpc.framework.foundation import stream
+
+
+class TestConsumer(stream.Consumer):
+ """A stream.Consumer instrumented for testing.
+
+ Attributes:
+ calls: A sequence of value-termination pairs describing the history of calls
+ made on this object.
+ """
+
+ def __init__(self):
+ self.calls = []
+
+ def consume(self, value):
+ """See stream.Consumer.consume for specification."""
+ self.calls.append((value, False))
+
+ def terminate(self):
+ """See stream.Consumer.terminate for specification."""
+ self.calls.append((None, True))
+
+ def consume_and_terminate(self, value):
+ """See stream.Consumer.consume_and_terminate for specification."""
+ self.calls.append((value, True))
+
+ def is_legal(self):
+ """Reports whether or not a legal sequence of calls has been made."""
+ terminated = False
+ for value, terminal in self.calls:
+ if terminated:
+ return False
+ elif terminal:
+ terminated = True
+ elif value is None:
+ return False
+ else: # pylint: disable=useless-else-on-loop
+ return True
+
+ def values(self):
+ """Returns the sequence of values that have been passed to this Consumer."""
+ return [value for value, _ in self.calls if value]
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/__init__.py b/src/python/grpcio/tests/unit/framework/interfaces/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/base/__init__.py b/src/python/grpcio/tests/unit/framework/interfaces/base/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/base/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/base/_control.py b/src/python/grpcio/tests/unit/framework/interfaces/base/_control.py
new file mode 100644
index 0000000000..38102b198a
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/base/_control.py
@@ -0,0 +1,568 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Part of the tests of the base interface of RPC Framework."""
+
+import abc
+import collections
+import enum
+import random # pylint: disable=unused-import
+import threading
+import time
+
+from grpc.framework.interfaces.base import base
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.base import _sequence
+from tests.unit.framework.interfaces.base import _state
+from tests.unit.framework.interfaces.base import test_interfaces # pylint: disable=unused-import
+
+_GROUP = 'base test cases test group'
+_METHOD = 'base test cases test method'
+
+_PAYLOAD_RANDOM_SECTION_MAXIMUM_SIZE = test_constants.PAYLOAD_SIZE / 20
+_MINIMUM_PAYLOAD_SIZE = test_constants.PAYLOAD_SIZE / 600
+
+
+def _create_payload(randomness):
+ length = randomness.randint(
+ _MINIMUM_PAYLOAD_SIZE, test_constants.PAYLOAD_SIZE)
+ random_section_length = randomness.randint(
+ 0, min(_PAYLOAD_RANDOM_SECTION_MAXIMUM_SIZE, length))
+ random_section = bytes(
+ bytearray(
+ randomness.getrandbits(8) for _ in range(random_section_length)))
+ sevens_section = '\x07' * (length - random_section_length)
+ return b''.join(randomness.sample((random_section, sevens_section), 2))
+
+
+def _anything_in_flight(state):
+ return (
+ state.invocation_initial_metadata_in_flight is not None or
+ state.invocation_payloads_in_flight or
+ state.invocation_completion_in_flight is not None or
+ state.service_initial_metadata_in_flight is not None or
+ state.service_payloads_in_flight or
+ state.service_completion_in_flight is not None or
+ 0 < state.invocation_allowance_in_flight or
+ 0 < state.service_allowance_in_flight
+ )
+
+
+def _verify_service_advance_and_update_state(
+ initial_metadata, payload, completion, allowance, state, implementation):
+ if initial_metadata is not None:
+ if state.invocation_initial_metadata_received:
+ return 'Later invocation initial metadata received: %s' % (
+ initial_metadata,)
+ if state.invocation_payloads_received:
+ return 'Invocation initial metadata received after payloads: %s' % (
+ state.invocation_payloads_received)
+ if state.invocation_completion_received:
+ return 'Invocation initial metadata received after invocation completion!'
+ if not implementation.metadata_transmitted(
+ state.invocation_initial_metadata_in_flight, initial_metadata):
+ return 'Invocation initial metadata maltransmitted: %s, %s' % (
+ state.invocation_initial_metadata_in_flight, initial_metadata)
+ else:
+ state.invocation_initial_metadata_in_flight = None
+ state.invocation_initial_metadata_received = True
+
+ if payload is not None:
+ if state.invocation_completion_received:
+ return 'Invocation payload received after invocation completion!'
+ elif not state.invocation_payloads_in_flight:
+ return 'Invocation payload "%s" received but not in flight!' % (payload,)
+ elif state.invocation_payloads_in_flight[0] != payload:
+ return 'Invocation payload mismatch: %s, %s' % (
+ state.invocation_payloads_in_flight[0], payload)
+ elif state.service_side_invocation_allowance < 1:
+ return 'Disallowed invocation payload!'
+ else:
+ state.invocation_payloads_in_flight.pop(0)
+ state.invocation_payloads_received += 1
+ state.service_side_invocation_allowance -= 1
+
+ if completion is not None:
+ if state.invocation_completion_received:
+ return 'Later invocation completion received: %s' % (completion,)
+ elif not implementation.completion_transmitted(
+ state.invocation_completion_in_flight, completion):
+ return 'Invocation completion maltransmitted: %s, %s' % (
+ state.invocation_completion_in_flight, completion)
+ else:
+ state.invocation_completion_in_flight = None
+ state.invocation_completion_received = True
+
+ if allowance is not None:
+ if allowance <= 0:
+ return 'Illegal allowance value: %s' % (allowance,)
+ else:
+ state.service_allowance_in_flight -= allowance
+ state.service_side_service_allowance += allowance
+
+
+def _verify_invocation_advance_and_update_state(
+ initial_metadata, payload, completion, allowance, state, implementation):
+ if initial_metadata is not None:
+ if state.service_initial_metadata_received:
+ return 'Later service initial metadata received: %s' % (initial_metadata,)
+ if state.service_payloads_received:
+ return 'Service initial metadata received after service payloads: %s' % (
+ state.service_payloads_received)
+ if state.service_completion_received:
+ return 'Service initial metadata received after service completion!'
+ if not implementation.metadata_transmitted(
+ state.service_initial_metadata_in_flight, initial_metadata):
+ return 'Service initial metadata maltransmitted: %s, %s' % (
+ state.service_initial_metadata_in_flight, initial_metadata)
+ else:
+ state.service_initial_metadata_in_flight = None
+ state.service_initial_metadata_received = True
+
+ if payload is not None:
+ if state.service_completion_received:
+ return 'Service payload received after service completion!'
+ elif not state.service_payloads_in_flight:
+ return 'Service payload "%s" received but not in flight!' % (payload,)
+ elif state.service_payloads_in_flight[0] != payload:
+ return 'Service payload mismatch: %s, %s' % (
+ state.invocation_payloads_in_flight[0], payload)
+ elif state.invocation_side_service_allowance < 1:
+ return 'Disallowed service payload!'
+ else:
+ state.service_payloads_in_flight.pop(0)
+ state.service_payloads_received += 1
+ state.invocation_side_service_allowance -= 1
+
+ if completion is not None:
+ if state.service_completion_received:
+ return 'Later service completion received: %s' % (completion,)
+ elif not implementation.completion_transmitted(
+ state.service_completion_in_flight, completion):
+ return 'Service completion maltransmitted: %s, %s' % (
+ state.service_completion_in_flight, completion)
+ else:
+ state.service_completion_in_flight = None
+ state.service_completion_received = True
+
+ if allowance is not None:
+ if allowance <= 0:
+ return 'Illegal allowance value: %s' % (allowance,)
+ else:
+ state.invocation_allowance_in_flight -= allowance
+ state.invocation_side_service_allowance += allowance
+
+
+class Invocation(
+ collections.namedtuple(
+ 'Invocation',
+ ('group', 'method', 'subscription_kind', 'timeout', 'initial_metadata',
+ 'payload', 'completion',))):
+ """A description of operation invocation.
+
+ Attributes:
+ group: The group identifier for the operation.
+ method: The method identifier for the operation.
+ subscription_kind: A base.Subscription.Kind value describing the kind of
+ subscription to use for the operation.
+ timeout: A duration in seconds to pass as the timeout value for the
+ operation.
+ initial_metadata: An object to pass as the initial metadata for the
+ operation or None.
+ payload: An object to pass as a payload value for the operation or None.
+ completion: An object to pass as a completion value for the operation or
+ None.
+ """
+
+
+class OnAdvance(
+ collections.namedtuple(
+ 'OnAdvance',
+ ('kind', 'initial_metadata', 'payload', 'completion', 'allowance'))):
+ """Describes action to be taken in a test in response to an advance call.
+
+ Attributes:
+ kind: A Kind value describing the overall kind of response.
+ initial_metadata: An initial metadata value to pass to a call of the advance
+ method of the operator under test. Only valid if kind is Kind.ADVANCE and
+ may be None.
+ payload: A payload value to pass to a call of the advance method of the
+ operator under test. Only valid if kind is Kind.ADVANCE and may be None.
+ completion: A base.Completion value to pass to a call of the advance method
+ of the operator under test. Only valid if kind is Kind.ADVANCE and may be
+ None.
+ allowance: An allowance value to pass to a call of the advance method of the
+ operator under test. Only valid if kind is Kind.ADVANCE and may be None.
+ """
+
+ @enum.unique
+ class Kind(enum.Enum):
+ ADVANCE = 'advance'
+ DEFECT = 'defect'
+ IDLE = 'idle'
+
+
+_DEFECT_ON_ADVANCE = OnAdvance(OnAdvance.Kind.DEFECT, None, None, None, None)
+_IDLE_ON_ADVANCE = OnAdvance(OnAdvance.Kind.IDLE, None, None, None, None)
+
+
+class Instruction(
+ collections.namedtuple(
+ 'Instruction',
+ ('kind', 'advance_args', 'advance_kwargs', 'conclude_success',
+ 'conclude_message', 'conclude_invocation_outcome_kind',
+ 'conclude_service_outcome_kind',))):
+ """"""
+
+ @enum.unique
+ class Kind(enum.Enum):
+ ADVANCE = 'ADVANCE'
+ CANCEL = 'CANCEL'
+ CONCLUDE = 'CONCLUDE'
+
+
+class Controller(object):
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def failed(self, message):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_request(self, request):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_request(self, serialized_request):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_response(self, response):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_response(self, serialized_response):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def invocation(self):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def poll(self):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def on_service_advance(
+ self, initial_metadata, payload, completion, allowance):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def on_invocation_advance(
+ self, initial_metadata, payload, completion, allowance):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def service_on_termination(self, outcome):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def invocation_on_termination(self, outcome):
+ """"""
+ raise NotImplementedError()
+
+
+class ControllerCreator(object):
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def name(self):
+ """"""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def controller(self, implementation, randomness):
+ """"""
+ raise NotImplementedError()
+
+
+class _Remainder(
+ collections.namedtuple(
+ '_Remainder',
+ ('invocation_payloads', 'service_payloads', 'invocation_completion',
+ 'service_completion',))):
+ """Describes work remaining to be done in a portion of a test.
+
+ Attributes:
+ invocation_payloads: The number of payloads to be sent from the invocation
+ side of the operation to the service side of the operation.
+ service_payloads: The number of payloads to be sent from the service side of
+ the operation to the invocation side of the operation.
+ invocation_completion: Whether or not completion from the invocation side of
+ the operation should be indicated and has yet to be indicated.
+ service_completion: Whether or not completion from the service side of the
+ operation should be indicated and has yet to be indicated.
+ """
+
+
+class _SequenceController(Controller):
+
+ def __init__(self, sequence, implementation, randomness):
+ """Constructor.
+
+ Args:
+ sequence: A _sequence.Sequence describing the steps to be taken in the
+ test at a relatively high level.
+ implementation: A test_interfaces.Implementation encapsulating the
+ base interface implementation that is the system under test.
+ randomness: A random.Random instance for use in the test.
+ """
+ self._condition = threading.Condition()
+ self._sequence = sequence
+ self._implementation = implementation
+ self._randomness = randomness
+
+ self._until = None
+ self._remaining_elements = None
+ self._poll_next = None
+ self._message = None
+
+ self._state = _state.OperationState()
+ self._todo = None
+
+ # called with self._condition
+ def _failed(self, message):
+ self._message = message
+ self._condition.notify_all()
+
+ def _passed(self, invocation_outcome, service_outcome):
+ self._poll_next = Instruction(
+ Instruction.Kind.CONCLUDE, None, None, True, None, invocation_outcome,
+ service_outcome)
+ self._condition.notify_all()
+
+ def failed(self, message):
+ with self._condition:
+ self._failed(message)
+
+ def serialize_request(self, request):
+ return request + request
+
+ def deserialize_request(self, serialized_request):
+ return serialized_request[:len(serialized_request) / 2]
+
+ def serialize_response(self, response):
+ return response * 3
+
+ def deserialize_response(self, serialized_response):
+ return serialized_response[2 * len(serialized_response) / 3:]
+
+ def invocation(self):
+ with self._condition:
+ self._until = time.time() + self._sequence.maximum_duration
+ self._remaining_elements = list(self._sequence.elements)
+ if self._sequence.invocation.initial_metadata:
+ initial_metadata = self._implementation.invocation_initial_metadata()
+ self._state.invocation_initial_metadata_in_flight = initial_metadata
+ else:
+ initial_metadata = None
+ if self._sequence.invocation.payload:
+ payload = _create_payload(self._randomness)
+ self._state.invocation_payloads_in_flight.append(payload)
+ else:
+ payload = None
+ if self._sequence.invocation.complete:
+ completion = self._implementation.invocation_completion()
+ self._state.invocation_completion_in_flight = completion
+ else:
+ completion = None
+ return Invocation(
+ _GROUP, _METHOD, base.Subscription.Kind.FULL,
+ self._sequence.invocation.timeout, initial_metadata, payload,
+ completion)
+
+ def poll(self):
+ with self._condition:
+ while True:
+ if self._message is not None:
+ return Instruction(
+ Instruction.Kind.CONCLUDE, None, None, False, self._message, None,
+ None)
+ elif self._poll_next:
+ poll_next = self._poll_next
+ self._poll_next = None
+ return poll_next
+ elif self._until < time.time():
+ return Instruction(
+ Instruction.Kind.CONCLUDE, None, None, False,
+ 'overran allotted time!', None, None)
+ else:
+ self._condition.wait(timeout=self._until-time.time())
+
+ def on_service_advance(
+ self, initial_metadata, payload, completion, allowance):
+ with self._condition:
+ message = _verify_service_advance_and_update_state(
+ initial_metadata, payload, completion, allowance, self._state,
+ self._implementation)
+ if message is not None:
+ self._failed(message)
+ if self._todo is not None:
+ raise ValueError('TODO!!!')
+ elif _anything_in_flight(self._state):
+ return _IDLE_ON_ADVANCE
+ elif self._remaining_elements:
+ element = self._remaining_elements.pop(0)
+ if element.kind is _sequence.Element.Kind.SERVICE_TRANSMISSION:
+ if element.transmission.initial_metadata:
+ initial_metadata = self._implementation.service_initial_metadata()
+ self._state.service_initial_metadata_in_flight = initial_metadata
+ else:
+ initial_metadata = None
+ if element.transmission.payload:
+ payload = _create_payload(self._randomness)
+ self._state.service_payloads_in_flight.append(payload)
+ self._state.service_side_service_allowance -= 1
+ else:
+ payload = None
+ if element.transmission.complete:
+ completion = self._implementation.service_completion()
+ self._state.service_completion_in_flight = completion
+ else:
+ completion = None
+ if (not self._state.invocation_completion_received and
+ 0 <= self._state.service_side_invocation_allowance):
+ allowance = 1
+ self._state.service_side_invocation_allowance += 1
+ self._state.invocation_allowance_in_flight += 1
+ else:
+ allowance = None
+ return OnAdvance(
+ OnAdvance.Kind.ADVANCE, initial_metadata, payload, completion,
+ allowance)
+ else:
+ raise ValueError('TODO!!!')
+ else:
+ return _IDLE_ON_ADVANCE
+
+ def on_invocation_advance(
+ self, initial_metadata, payload, completion, allowance):
+ with self._condition:
+ message = _verify_invocation_advance_and_update_state(
+ initial_metadata, payload, completion, allowance, self._state,
+ self._implementation)
+ if message is not None:
+ self._failed(message)
+ if self._todo is not None:
+ raise ValueError('TODO!!!')
+ elif _anything_in_flight(self._state):
+ return _IDLE_ON_ADVANCE
+ elif self._remaining_elements:
+ element = self._remaining_elements.pop(0)
+ if element.kind is _sequence.Element.Kind.INVOCATION_TRANSMISSION:
+ if element.transmission.initial_metadata:
+ initial_metadata = self._implementation.invocation_initial_metadata()
+ self._state.invocation_initial_metadata_in_fight = initial_metadata
+ else:
+ initial_metadata = None
+ if element.transmission.payload:
+ payload = _create_payload(self._randomness)
+ self._state.invocation_payloads_in_flight.append(payload)
+ self._state.invocation_side_invocation_allowance -= 1
+ else:
+ payload = None
+ if element.transmission.complete:
+ completion = self._implementation.invocation_completion()
+ self._state.invocation_completion_in_flight = completion
+ else:
+ completion = None
+ if (not self._state.service_completion_received and
+ 0 <= self._state.invocation_side_service_allowance):
+ allowance = 1
+ self._state.invocation_side_service_allowance += 1
+ self._state.service_allowance_in_flight += 1
+ else:
+ allowance = None
+ return OnAdvance(
+ OnAdvance.Kind.ADVANCE, initial_metadata, payload, completion,
+ allowance)
+ else:
+ raise ValueError('TODO!!!')
+ else:
+ return _IDLE_ON_ADVANCE
+
+ def service_on_termination(self, outcome):
+ with self._condition:
+ self._state.service_side_outcome = outcome
+ if self._todo is not None or self._remaining_elements:
+ self._failed('Premature service-side outcome %s!' % (outcome,))
+ elif outcome.kind is not self._sequence.outcome_kinds.service:
+ self._failed(
+ 'Incorrect service-side outcome kind: %s should have been %s' % (
+ outcome.kind, self._sequence.outcome_kinds.service))
+ elif self._state.invocation_side_outcome is not None:
+ self._passed(self._state.invocation_side_outcome.kind, outcome.kind)
+
+ def invocation_on_termination(self, outcome):
+ with self._condition:
+ self._state.invocation_side_outcome = outcome
+ if self._todo is not None or self._remaining_elements:
+ self._failed('Premature invocation-side outcome %s!' % (outcome,))
+ elif outcome.kind is not self._sequence.outcome_kinds.invocation:
+ self._failed(
+ 'Incorrect invocation-side outcome kind: %s should have been %s' % (
+ outcome.kind, self._sequence.outcome_kinds.invocation))
+ elif self._state.service_side_outcome is not None:
+ self._passed(outcome.kind, self._state.service_side_outcome.kind)
+
+
+class _SequenceControllerCreator(ControllerCreator):
+
+ def __init__(self, sequence):
+ self._sequence = sequence
+
+ def name(self):
+ return self._sequence.name
+
+ def controller(self, implementation, randomness):
+ return _SequenceController(self._sequence, implementation, randomness)
+
+
+CONTROLLER_CREATORS = tuple(
+ _SequenceControllerCreator(sequence) for sequence in _sequence.SEQUENCES)
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/base/_sequence.py b/src/python/grpcio/tests/unit/framework/interfaces/base/_sequence.py
new file mode 100644
index 0000000000..571d0e1e63
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/base/_sequence.py
@@ -0,0 +1,171 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Part of the tests of the base interface of RPC Framework."""
+
+import collections
+import enum
+
+from grpc.framework.interfaces.base import base
+from tests.unit.framework.common import test_constants
+
+
+class Invocation(
+ collections.namedtuple(
+ 'Invocation', ('timeout', 'initial_metadata', 'payload', 'complete',))):
+ """A recipe for operation invocation.
+
+ Attributes:
+ timeout: A duration in seconds to pass to the system under test as the
+ operation's timeout value.
+ initial_metadata: A boolean indicating whether or not to pass initial
+ metadata when invoking the operation.
+ payload: A boolean indicating whether or not to pass a payload when
+ invoking the operation.
+ complete: A boolean indicating whether or not to indicate completion of
+ transmissions from the invoking side of the operation when invoking the
+ operation.
+ """
+
+
+class Transmission(
+ collections.namedtuple(
+ 'Transmission', ('initial_metadata', 'payload', 'complete',))):
+ """A recipe for a single transmission in an operation.
+
+ Attributes:
+ initial_metadata: A boolean indicating whether or not to pass initial
+ metadata as part of the transmission.
+ payload: A boolean indicating whether or not to pass a payload as part of
+ the transmission.
+ complete: A boolean indicating whether or not to indicate completion of
+ transmission from the transmitting side of the operation as part of the
+ transmission.
+ """
+
+
+class Intertransmission(
+ collections.namedtuple('Intertransmission', ('invocation', 'service',))):
+ """A recipe for multiple transmissions in an operation.
+
+ Attributes:
+ invocation: An integer describing the number of payloads to send from the
+ invocation side of the operation to the service side.
+ service: An integer describing the number of payloads to send from the
+ service side of the operation to the invocation side.
+ """
+
+
+class Element(collections.namedtuple('Element', ('kind', 'transmission',))):
+ """A sum type for steps to perform when testing an operation.
+
+ Attributes:
+ kind: A Kind value describing the kind of step to perform in the test.
+ transmission: Only valid for kinds Kind.INVOCATION_TRANSMISSION and
+ Kind.SERVICE_TRANSMISSION, a Transmission value describing the details of
+ the transmission to be made.
+ """
+
+ @enum.unique
+ class Kind(enum.Enum):
+ INVOCATION_TRANSMISSION = 'invocation transmission'
+ SERVICE_TRANSMISSION = 'service transmission'
+ INTERTRANSMISSION = 'intertransmission'
+ INVOCATION_CANCEL = 'invocation cancel'
+ SERVICE_CANCEL = 'service cancel'
+ INVOCATION_FAILURE = 'invocation failure'
+ SERVICE_FAILURE = 'service failure'
+
+
+class OutcomeKinds(
+ collections.namedtuple('Outcome', ('invocation', 'service',))):
+ """A description of the expected outcome of an operation test.
+
+ Attributes:
+ invocation: The base.Outcome.Kind value expected on the invocation side of
+ the operation.
+ service: The base.Outcome.Kind value expected on the service side of the
+ operation.
+ """
+
+
+class Sequence(
+ collections.namedtuple(
+ 'Sequence',
+ ('name', 'maximum_duration', 'invocation', 'elements',
+ 'outcome_kinds',))):
+ """Describes at a high level steps to perform in a test.
+
+ Attributes:
+ name: The string name of the sequence.
+ maximum_duration: A length of time in seconds to allow for the test before
+ declaring it to have failed.
+ invocation: An Invocation value describing how to invoke the operation
+ under test.
+ elements: A sequence of Element values describing at coarse granularity
+ actions to take during the operation under test.
+ outcome_kinds: An OutcomeKinds value describing the expected outcome kinds
+ of the test.
+ """
+
+_EASY = Sequence(
+ 'Easy',
+ test_constants.TIME_ALLOWANCE,
+ Invocation(test_constants.LONG_TIMEOUT, True, True, True),
+ (
+ Element(
+ Element.Kind.SERVICE_TRANSMISSION, Transmission(True, True, True)),
+ ),
+ OutcomeKinds(base.Outcome.Kind.COMPLETED, base.Outcome.Kind.COMPLETED))
+
+_PEASY = Sequence(
+ 'Peasy',
+ test_constants.TIME_ALLOWANCE,
+ Invocation(test_constants.LONG_TIMEOUT, True, True, False),
+ (
+ Element(
+ Element.Kind.SERVICE_TRANSMISSION, Transmission(True, True, False)),
+ Element(
+ Element.Kind.INVOCATION_TRANSMISSION,
+ Transmission(False, True, True)),
+ Element(
+ Element.Kind.SERVICE_TRANSMISSION, Transmission(False, True, True)),
+ ),
+ OutcomeKinds(base.Outcome.Kind.COMPLETED, base.Outcome.Kind.COMPLETED))
+
+
+# TODO(issue 2959): Finish this test suite. This tuple of sequences should
+# contain at least the values in the Cartesian product of (half-duplex,
+# full-duplex) * (zero payloads, one payload, test_constants.STREAM_LENGTH
+# payloads) * (completion, cancellation, expiration, programming defect in
+# servicer code).
+SEQUENCES = (
+ _EASY,
+ _PEASY,
+)
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/base/_state.py b/src/python/grpcio/tests/unit/framework/interfaces/base/_state.py
new file mode 100644
index 0000000000..21cf33aeb6
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/base/_state.py
@@ -0,0 +1,55 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Part of the tests of the base interface of RPC Framework."""
+
+
+class OperationState(object):
+
+ def __init__(self):
+ self.invocation_initial_metadata_in_flight = None
+ self.invocation_initial_metadata_received = False
+ self.invocation_payloads_in_flight = []
+ self.invocation_payloads_received = 0
+ self.invocation_completion_in_flight = None
+ self.invocation_completion_received = False
+ self.service_initial_metadata_in_flight = None
+ self.service_initial_metadata_received = False
+ self.service_payloads_in_flight = []
+ self.service_payloads_received = 0
+ self.service_completion_in_flight = None
+ self.service_completion_received = False
+ self.invocation_side_invocation_allowance = 1
+ self.invocation_side_service_allowance = 1
+ self.service_side_invocation_allowance = 1
+ self.service_side_service_allowance = 1
+ self.invocation_allowance_in_flight = 0
+ self.service_allowance_in_flight = 0
+ self.invocation_side_outcome = None
+ self.service_side_outcome = None
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/base/test_cases.py b/src/python/grpcio/tests/unit/framework/interfaces/base/test_cases.py
new file mode 100644
index 0000000000..4f8e26c9a2
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/base/test_cases.py
@@ -0,0 +1,277 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests of the base interface of RPC Framework."""
+
+import logging
+import random
+import threading
+import time
+import unittest
+
+from grpc.framework.foundation import logging_pool
+from grpc.framework.interfaces.base import base
+from grpc.framework.interfaces.base import utilities
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.base import _control
+from tests.unit.framework.interfaces.base import test_interfaces
+
+_SYNCHRONICITY_VARIATION = (('Sync', False), ('Async', True))
+
+_EMPTY_OUTCOME_KIND_DICT = {
+ outcome_kind: 0 for outcome_kind in base.Outcome.Kind}
+
+
+class _Serialization(test_interfaces.Serialization):
+
+ def serialize_request(self, request):
+ return request + request
+
+ def deserialize_request(self, serialized_request):
+ return serialized_request[:len(serialized_request) / 2]
+
+ def serialize_response(self, response):
+ return response * 3
+
+ def deserialize_response(self, serialized_response):
+ return serialized_response[2 * len(serialized_response) / 3:]
+
+
+def _advance(quadruples, operator, controller):
+ try:
+ for quadruple in quadruples:
+ operator.advance(
+ initial_metadata=quadruple[0], payload=quadruple[1],
+ completion=quadruple[2], allowance=quadruple[3])
+ except Exception as e: # pylint: disable=broad-except
+ controller.failed('Exception on advance: %e' % e)
+
+
+class _Operator(base.Operator):
+
+ def __init__(self, controller, on_advance, pool, operator_under_test):
+ self._condition = threading.Condition()
+ self._controller = controller
+ self._on_advance = on_advance
+ self._pool = pool
+ self._operator_under_test = operator_under_test
+ self._pending_advances = []
+
+ def set_operator_under_test(self, operator_under_test):
+ with self._condition:
+ self._operator_under_test = operator_under_test
+ pent_advances = self._pending_advances
+ self._pending_advances = []
+ pool = self._pool
+ controller = self._controller
+
+ if pool is None:
+ _advance(pent_advances, operator_under_test, controller)
+ else:
+ pool.submit(_advance, pent_advances, operator_under_test, controller)
+
+ def advance(
+ self, initial_metadata=None, payload=None, completion=None,
+ allowance=None):
+ on_advance = self._on_advance(
+ initial_metadata, payload, completion, allowance)
+ if on_advance.kind is _control.OnAdvance.Kind.ADVANCE:
+ with self._condition:
+ pool = self._pool
+ operator_under_test = self._operator_under_test
+ controller = self._controller
+
+ quadruple = (
+ on_advance.initial_metadata, on_advance.payload,
+ on_advance.completion, on_advance.allowance)
+ if pool is None:
+ _advance((quadruple,), operator_under_test, controller)
+ else:
+ pool.submit(_advance, (quadruple,), operator_under_test, controller)
+ elif on_advance.kind is _control.OnAdvance.Kind.DEFECT:
+ raise ValueError(
+ 'Deliberately raised exception from Operator.advance (in a test)!')
+
+
+class _ProtocolReceiver(base.ProtocolReceiver):
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._contexts = []
+
+ def context(self, protocol_context):
+ with self._condition:
+ self._contexts.append(protocol_context)
+
+
+class _Servicer(base.Servicer):
+ """A base.Servicer with instrumented for testing."""
+
+ def __init__(self, group, method, controllers, pool):
+ self._condition = threading.Condition()
+ self._group = group
+ self._method = method
+ self._pool = pool
+ self._controllers = list(controllers)
+
+ def service(self, group, method, context, output_operator):
+ with self._condition:
+ controller = self._controllers.pop(0)
+ if group != self._group or method != self._method:
+ controller.fail(
+ '%s != %s or %s != %s' % (group, self._group, method, self._method))
+ raise base.NoSuchMethodError(None, None)
+ else:
+ operator = _Operator(
+ controller, controller.on_service_advance, self._pool,
+ output_operator)
+ outcome = context.add_termination_callback(
+ controller.service_on_termination)
+ if outcome is not None:
+ controller.service_on_termination(outcome)
+ return utilities.full_subscription(operator, _ProtocolReceiver())
+
+
+class _OperationTest(unittest.TestCase):
+
+ def setUp(self):
+ if self._synchronicity_variation:
+ self._pool = logging_pool.pool(test_constants.POOL_SIZE)
+ else:
+ self._pool = None
+ self._controller = self._controller_creator.controller(
+ self._implementation, self._randomness)
+
+ def tearDown(self):
+ if self._synchronicity_variation:
+ self._pool.shutdown(wait=True)
+ else:
+ self._pool = None
+
+ def test_operation(self):
+ invocation = self._controller.invocation()
+ if invocation.subscription_kind is base.Subscription.Kind.FULL:
+ test_operator = _Operator(
+ self._controller, self._controller.on_invocation_advance,
+ self._pool, None)
+ subscription = utilities.full_subscription(
+ test_operator, _ProtocolReceiver())
+ else:
+ # TODO(nathaniel): support and test other subscription kinds.
+ self.fail('Non-full subscriptions not yet supported!')
+
+ servicer = _Servicer(
+ invocation.group, invocation.method, (self._controller,), self._pool)
+
+ invocation_end, service_end, memo = self._implementation.instantiate(
+ {(invocation.group, invocation.method): _Serialization()}, servicer)
+
+ try:
+ invocation_end.start()
+ service_end.start()
+ operation_context, operator_under_test = invocation_end.operate(
+ invocation.group, invocation.method, subscription, invocation.timeout,
+ initial_metadata=invocation.initial_metadata, payload=invocation.payload,
+ completion=invocation.completion)
+ test_operator.set_operator_under_test(operator_under_test)
+ outcome = operation_context.add_termination_callback(
+ self._controller.invocation_on_termination)
+ if outcome is not None:
+ self._controller.invocation_on_termination(outcome)
+ except Exception as e: # pylint: disable=broad-except
+ self._controller.failed('Exception on invocation: %s' % e)
+ self.fail(e)
+
+ while True:
+ instruction = self._controller.poll()
+ if instruction.kind is _control.Instruction.Kind.ADVANCE:
+ try:
+ test_operator.advance(
+ *instruction.advance_args, **instruction.advance_kwargs)
+ except Exception as e: # pylint: disable=broad-except
+ self._controller.failed('Exception on instructed advance: %s' % e)
+ elif instruction.kind is _control.Instruction.Kind.CANCEL:
+ try:
+ operation_context.cancel()
+ except Exception as e: # pylint: disable=broad-except
+ self._controller.failed('Exception on cancel: %s' % e)
+ elif instruction.kind is _control.Instruction.Kind.CONCLUDE:
+ break
+
+ invocation_stop_event = invocation_end.stop(0)
+ service_stop_event = service_end.stop(0)
+ invocation_stop_event.wait()
+ service_stop_event.wait()
+ invocation_stats = invocation_end.operation_stats()
+ service_stats = service_end.operation_stats()
+
+ self._implementation.destantiate(memo)
+
+ self.assertTrue(
+ instruction.conclude_success, msg=instruction.conclude_message)
+
+ expected_invocation_stats = dict(_EMPTY_OUTCOME_KIND_DICT)
+ expected_invocation_stats[
+ instruction.conclude_invocation_outcome_kind] += 1
+ self.assertDictEqual(expected_invocation_stats, invocation_stats)
+ expected_service_stats = dict(_EMPTY_OUTCOME_KIND_DICT)
+ expected_service_stats[instruction.conclude_service_outcome_kind] += 1
+ self.assertDictEqual(expected_service_stats, service_stats)
+
+
+def test_cases(implementation):
+ """Creates unittest.TestCase classes for a given Base implementation.
+
+ Args:
+ implementation: A test_interfaces.Implementation specifying creation and
+ destruction of the Base implementation under test.
+
+ Returns:
+ A sequence of subclasses of unittest.TestCase defining tests of the
+ specified Base layer implementation.
+ """
+ random_seed = hash(time.time())
+ logging.warning('Random seed for this execution: %s', random_seed)
+ randomness = random.Random(x=random_seed)
+
+ test_case_classes = []
+ for synchronicity_variation in _SYNCHRONICITY_VARIATION:
+ for controller_creator in _control.CONTROLLER_CREATORS:
+ name = ''.join(
+ (synchronicity_variation[0], controller_creator.name(), 'Test',))
+ test_case_classes.append(
+ type(name, (_OperationTest,),
+ {'_implementation': implementation,
+ '_randomness': randomness,
+ '_synchronicity_variation': synchronicity_variation[1],
+ '_controller_creator': controller_creator,
+ '__module__': implementation.__module__,
+ }))
+
+ return test_case_classes
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/base/test_interfaces.py b/src/python/grpcio/tests/unit/framework/interfaces/base/test_interfaces.py
new file mode 100644
index 0000000000..84afd24d47
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/base/test_interfaces.py
@@ -0,0 +1,186 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Interfaces used in tests of implementations of the Base layer."""
+
+import abc
+
+from grpc.framework.interfaces.base import base # pylint: disable=unused-import
+
+
+class Serialization(object):
+ """Specifies serialization and deserialization of test payloads."""
+ __metaclass__ = abc.ABCMeta
+
+ def serialize_request(self, request):
+ """Serializes a request value used in a test.
+
+ Args:
+ request: A request value created by a test.
+
+ Returns:
+ A bytestring that is the serialization of the given request.
+ """
+ raise NotImplementedError()
+
+ def deserialize_request(self, serialized_request):
+ """Deserializes a request value used in a test.
+
+ Args:
+ serialized_request: A bytestring that is the serialization of some request
+ used in a test.
+
+ Returns:
+ The request value encoded by the given bytestring.
+ """
+ raise NotImplementedError()
+
+ def serialize_response(self, response):
+ """Serializes a response value used in a test.
+
+ Args:
+ response: A response value created by a test.
+
+ Returns:
+ A bytestring that is the serialization of the given response.
+ """
+ raise NotImplementedError()
+
+ def deserialize_response(self, serialized_response):
+ """Deserializes a response value used in a test.
+
+ Args:
+ serialized_response: A bytestring that is the serialization of some
+ response used in a test.
+
+ Returns:
+ The response value encoded by the given bytestring.
+ """
+ raise NotImplementedError()
+
+
+class Implementation(object):
+ """Specifies an implementation of the Base layer."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def instantiate(self, serializations, servicer):
+ """Instantiates the Base layer implementation to be used in a test.
+
+ Args:
+ serializations: A dict from group-method pair to Serialization object
+ specifying how to serialize and deserialize payload values used in the
+ test.
+ servicer: A base.Servicer object to be called to service RPCs made during
+ the test.
+
+ Returns:
+ A sequence of length three the first element of which is a
+ base.End to be used to invoke RPCs, the second element of which is a
+ base.End to be used to service invoked RPCs, and the third element of
+ which is an arbitrary memo object to be kept and passed to destantiate
+ at the conclusion of the test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def destantiate(self, memo):
+ """Destroys the Base layer implementation under test.
+
+ Args:
+ memo: The object from the third position of the return value of a call to
+ instantiate.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def invocation_initial_metadata(self):
+ """Provides an operation's invocation-side initial metadata.
+
+ Returns:
+ A value to use for an operation's invocation-side initial metadata, or
+ None.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def service_initial_metadata(self):
+ """Provides an operation's service-side initial metadata.
+
+ Returns:
+ A value to use for an operation's service-side initial metadata, or
+ None.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def invocation_completion(self):
+ """Provides an operation's invocation-side completion.
+
+ Returns:
+ A base.Completion to use for an operation's invocation-side completion.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def service_completion(self):
+ """Provides an operation's service-side completion.
+
+ Returns:
+ A base.Completion to use for an operation's service-side completion.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def metadata_transmitted(self, original_metadata, transmitted_metadata):
+ """Identifies whether or not metadata was properly transmitted.
+
+ Args:
+ original_metadata: A metadata value passed to the system under test.
+ transmitted_metadata: The same metadata value after having been
+ transmitted through the system under test.
+
+ Returns:
+ Whether or not the metadata was properly transmitted.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def completion_transmitted(self, original_completion, transmitted_completion):
+ """Identifies whether or not a base.Completion was properly transmitted.
+
+ Args:
+ original_completion: A base.Completion passed to the system under test.
+ transmitted_completion: The same completion value after having been
+ transmitted through the system under test.
+
+ Returns:
+ Whether or not the completion was properly transmitted.
+ """
+ raise NotImplementedError()
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_3069_test_constant.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_3069_test_constant.py
new file mode 100644
index 0000000000..1ea356c0bf
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_3069_test_constant.py
@@ -0,0 +1,37 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A test constant working around issue 3069."""
+
+# test_constants is referenced from specification in this module.
+from tests.unit.framework.common import test_constants # pylint: disable=unused-import
+
+# TODO(issue 3069): Replace uses of this constant with
+# test_constants.SHORT_TIMEOUT.
+REALLY_SHORT_TIMEOUT = 0.1
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/__init__.py b/src/python/grpcio/tests/unit/framework/interfaces/face/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_blocking_invocation_inline_service.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_blocking_invocation_inline_service.py
new file mode 100644
index 0000000000..3bcefa601d
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_blocking_invocation_inline_service.py
@@ -0,0 +1,252 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Test code for the Face layer of RPC Framework."""
+
+import abc
+import unittest
+
+# test_interfaces is referenced from specification in this module.
+from grpc.framework.interfaces.face import face
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.common import test_control
+from tests.unit.framework.common import test_coverage
+from tests.unit.framework.interfaces.face import _3069_test_constant
+from tests.unit.framework.interfaces.face import _digest
+from tests.unit.framework.interfaces.face import _stock_service
+from tests.unit.framework.interfaces.face import test_interfaces # pylint: disable=unused-import
+
+
+class TestCase(test_coverage.Coverage, unittest.TestCase):
+ """A test of the Face layer of RPC Framework.
+
+ Concrete subclasses must have an "implementation" attribute of type
+ test_interfaces.Implementation and an "invoker_constructor" attribute of type
+ _invocation.InvokerConstructor.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ NAME = 'BlockingInvocationInlineServiceTest'
+
+ def setUp(self):
+ """See unittest.TestCase.setUp for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self._control = test_control.PauseFailControl()
+ self._digest = _digest.digest(
+ _stock_service.STOCK_TEST_SERVICE, self._control, None)
+
+ generic_stub, dynamic_stubs, self._memo = self.implementation.instantiate(
+ self._digest.methods, self._digest.inline_method_implementations, None)
+ self._invoker = self.invoker_constructor.construct_invoker(
+ generic_stub, dynamic_stubs, self._digest.methods)
+
+ def tearDown(self):
+ """See unittest.TestCase.tearDown for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self._invoker = None
+ self.implementation.destantiate(self._memo)
+
+ def testSuccessfulUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ response, call = self._invoker.blocking(group, method)(
+ request, test_constants.LONG_TIMEOUT, with_call=True)
+
+ test_messages.verify(request, response, self)
+
+ def testSuccessfulUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ response_iterator = self._invoker.blocking(group, method)(
+ request, test_constants.LONG_TIMEOUT)
+ responses = list(response_iterator)
+
+ test_messages.verify(request, responses, self)
+
+ def testSuccessfulStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ response, call = self._invoker.blocking(group, method)(
+ iter(requests), test_constants.LONG_TIMEOUT, with_call=True)
+
+ test_messages.verify(requests, response, self)
+
+ def testSuccessfulStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ response_iterator = self._invoker.blocking(group, method)(
+ iter(requests), test_constants.LONG_TIMEOUT)
+ responses = list(response_iterator)
+
+ test_messages.verify(requests, responses, self)
+
+ def testSequentialInvocations(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ second_request = test_messages.request()
+
+ first_response = self._invoker.blocking(group, method)(
+ first_request, test_constants.LONG_TIMEOUT)
+
+ test_messages.verify(first_request, first_response, self)
+
+ second_response = self._invoker.blocking(group, method)(
+ second_request, test_constants.LONG_TIMEOUT)
+
+ test_messages.verify(second_request, second_response, self)
+
+ @unittest.skip('Parallel invocations impossible with blocking control flow!')
+ def testParallelInvocations(self):
+ raise NotImplementedError()
+
+ @unittest.skip('Parallel invocations impossible with blocking control flow!')
+ def testWaitingForSomeButNotAllParallelInvocations(self):
+ raise NotImplementedError()
+
+ @unittest.skip('Cancellation impossible with blocking control flow!')
+ def testCancelledUnaryRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @unittest.skip('Cancellation impossible with blocking control flow!')
+ def testCancelledUnaryRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ @unittest.skip('Cancellation impossible with blocking control flow!')
+ def testCancelledStreamRequestUnaryResponse(self):
+ raise NotImplementedError()
+
+ @unittest.skip('Cancellation impossible with blocking control flow!')
+ def testCancelledStreamRequestStreamResponse(self):
+ raise NotImplementedError()
+
+ def testExpiredUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self._control.pause(), self.assertRaises(
+ face.ExpirationError):
+ self._invoker.blocking(group, method)(
+ request, _3069_test_constant.REALLY_SHORT_TIMEOUT)
+
+ def testExpiredUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self._control.pause(), self.assertRaises(
+ face.ExpirationError):
+ response_iterator = self._invoker.blocking(group, method)(
+ request, _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ list(response_iterator)
+
+ def testExpiredStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self._control.pause(), self.assertRaises(
+ face.ExpirationError):
+ self._invoker.blocking(group, method)(
+ iter(requests), _3069_test_constant.REALLY_SHORT_TIMEOUT)
+
+ def testExpiredStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self._control.pause(), self.assertRaises(
+ face.ExpirationError):
+ response_iterator = self._invoker.blocking(group, method)(
+ iter(requests), _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ list(response_iterator)
+
+ def testFailedUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self._control.fail(), self.assertRaises(face.RemoteError):
+ self._invoker.blocking(group, method)(
+ request, test_constants.LONG_TIMEOUT)
+
+ def testFailedUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self._control.fail(), self.assertRaises(face.RemoteError):
+ response_iterator = self._invoker.blocking(group, method)(
+ request, test_constants.LONG_TIMEOUT)
+ list(response_iterator)
+
+ def testFailedStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self._control.fail(), self.assertRaises(face.RemoteError):
+ self._invoker.blocking(group, method)(
+ iter(requests), test_constants.LONG_TIMEOUT)
+
+ def testFailedStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self._control.fail(), self.assertRaises(face.RemoteError):
+ response_iterator = self._invoker.blocking(group, method)(
+ iter(requests), test_constants.LONG_TIMEOUT)
+ list(response_iterator)
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_digest.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_digest.py
new file mode 100644
index 0000000000..9304b6b1db
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_digest.py
@@ -0,0 +1,444 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Code for making a service.TestService more amenable to use in tests."""
+
+import collections
+import threading
+
+# test_control, _service, and test_interfaces are referenced from specification
+# in this module.
+from grpc.framework.common import cardinality
+from grpc.framework.common import style
+from grpc.framework.foundation import stream
+from grpc.framework.foundation import stream_util
+from grpc.framework.interfaces.face import face
+from tests.unit.framework.common import test_control # pylint: disable=unused-import
+from tests.unit.framework.interfaces.face import _service # pylint: disable=unused-import
+from tests.unit.framework.interfaces.face import test_interfaces # pylint: disable=unused-import
+
+_IDENTITY = lambda x: x
+
+
+class TestServiceDigest(
+ collections.namedtuple(
+ 'TestServiceDigest',
+ ('methods',
+ 'inline_method_implementations',
+ 'event_method_implementations',
+ 'multi_method_implementation',
+ 'unary_unary_messages_sequences',
+ 'unary_stream_messages_sequences',
+ 'stream_unary_messages_sequences',
+ 'stream_stream_messages_sequences',))):
+ """A transformation of a service.TestService.
+
+ Attributes:
+ methods: A dict from method group-name pair to test_interfaces.Method object
+ describing the RPC methods that may be called during the test.
+ inline_method_implementations: A dict from method group-name pair to
+ face.MethodImplementation object to be used in tests of in-line calls to
+ behaviors under test.
+ event_method_implementations: A dict from method group-name pair to
+ face.MethodImplementation object to be used in tests of event-driven calls
+ to behaviors under test.
+ multi_method_implementation: A face.MultiMethodImplementation to be used in
+ tests of generic calls to behaviors under test.
+ unary_unary_messages_sequences: A dict from method group-name pair to
+ sequence of service.UnaryUnaryTestMessages objects to be used to test the
+ identified method.
+ unary_stream_messages_sequences: A dict from method group-name pair to
+ sequence of service.UnaryStreamTestMessages objects to be used to test the
+ identified method.
+ stream_unary_messages_sequences: A dict from method group-name pair to
+ sequence of service.StreamUnaryTestMessages objects to be used to test the
+ identified method.
+ stream_stream_messages_sequences: A dict from method group-name pair to
+ sequence of service.StreamStreamTestMessages objects to be used to test
+ the identified method.
+ """
+
+
+class _BufferingConsumer(stream.Consumer):
+ """A trivial Consumer that dumps what it consumes in a user-mutable buffer."""
+
+ def __init__(self):
+ self.consumed = []
+ self.terminated = False
+
+ def consume(self, value):
+ self.consumed.append(value)
+
+ def terminate(self):
+ self.terminated = True
+
+ def consume_and_terminate(self, value):
+ self.consumed.append(value)
+ self.terminated = True
+
+
+class _InlineUnaryUnaryMethod(face.MethodImplementation):
+
+ def __init__(self, unary_unary_test_method, control):
+ self._test_method = unary_unary_test_method
+ self._control = control
+
+ self.cardinality = cardinality.Cardinality.UNARY_UNARY
+ self.style = style.Service.INLINE
+
+ def unary_unary_inline(self, request, context):
+ response_list = []
+ self._test_method.service(
+ request, response_list.append, context, self._control)
+ return response_list.pop(0)
+
+
+class _EventUnaryUnaryMethod(face.MethodImplementation):
+
+ def __init__(self, unary_unary_test_method, control, pool):
+ self._test_method = unary_unary_test_method
+ self._control = control
+ self._pool = pool
+
+ self.cardinality = cardinality.Cardinality.UNARY_UNARY
+ self.style = style.Service.EVENT
+
+ def unary_unary_event(self, request, response_callback, context):
+ if self._pool is None:
+ self._test_method.service(
+ request, response_callback, context, self._control)
+ else:
+ self._pool.submit(
+ self._test_method.service, request, response_callback, context,
+ self._control)
+
+
+class _InlineUnaryStreamMethod(face.MethodImplementation):
+
+ def __init__(self, unary_stream_test_method, control):
+ self._test_method = unary_stream_test_method
+ self._control = control
+
+ self.cardinality = cardinality.Cardinality.UNARY_STREAM
+ self.style = style.Service.INLINE
+
+ def unary_stream_inline(self, request, context):
+ response_consumer = _BufferingConsumer()
+ self._test_method.service(
+ request, response_consumer, context, self._control)
+ for response in response_consumer.consumed:
+ yield response
+
+
+class _EventUnaryStreamMethod(face.MethodImplementation):
+
+ def __init__(self, unary_stream_test_method, control, pool):
+ self._test_method = unary_stream_test_method
+ self._control = control
+ self._pool = pool
+
+ self.cardinality = cardinality.Cardinality.UNARY_STREAM
+ self.style = style.Service.EVENT
+
+ def unary_stream_event(self, request, response_consumer, context):
+ if self._pool is None:
+ self._test_method.service(
+ request, response_consumer, context, self._control)
+ else:
+ self._pool.submit(
+ self._test_method.service, request, response_consumer, context,
+ self._control)
+
+
+class _InlineStreamUnaryMethod(face.MethodImplementation):
+
+ def __init__(self, stream_unary_test_method, control):
+ self._test_method = stream_unary_test_method
+ self._control = control
+
+ self.cardinality = cardinality.Cardinality.STREAM_UNARY
+ self.style = style.Service.INLINE
+
+ def stream_unary_inline(self, request_iterator, context):
+ response_list = []
+ request_consumer = self._test_method.service(
+ response_list.append, context, self._control)
+ for request in request_iterator:
+ request_consumer.consume(request)
+ request_consumer.terminate()
+ return response_list.pop(0)
+
+
+class _EventStreamUnaryMethod(face.MethodImplementation):
+
+ def __init__(self, stream_unary_test_method, control, pool):
+ self._test_method = stream_unary_test_method
+ self._control = control
+ self._pool = pool
+
+ self.cardinality = cardinality.Cardinality.STREAM_UNARY
+ self.style = style.Service.EVENT
+
+ def stream_unary_event(self, response_callback, context):
+ request_consumer = self._test_method.service(
+ response_callback, context, self._control)
+ if self._pool is None:
+ return request_consumer
+ else:
+ return stream_util.ThreadSwitchingConsumer(request_consumer, self._pool)
+
+
+class _InlineStreamStreamMethod(face.MethodImplementation):
+
+ def __init__(self, stream_stream_test_method, control):
+ self._test_method = stream_stream_test_method
+ self._control = control
+
+ self.cardinality = cardinality.Cardinality.STREAM_STREAM
+ self.style = style.Service.INLINE
+
+ def stream_stream_inline(self, request_iterator, context):
+ response_consumer = _BufferingConsumer()
+ request_consumer = self._test_method.service(
+ response_consumer, context, self._control)
+
+ for request in request_iterator:
+ request_consumer.consume(request)
+ while response_consumer.consumed:
+ yield response_consumer.consumed.pop(0)
+ response_consumer.terminate()
+
+
+class _EventStreamStreamMethod(face.MethodImplementation):
+
+ def __init__(self, stream_stream_test_method, control, pool):
+ self._test_method = stream_stream_test_method
+ self._control = control
+ self._pool = pool
+
+ self.cardinality = cardinality.Cardinality.STREAM_STREAM
+ self.style = style.Service.EVENT
+
+ def stream_stream_event(self, response_consumer, context):
+ request_consumer = self._test_method.service(
+ response_consumer, context, self._control)
+ if self._pool is None:
+ return request_consumer
+ else:
+ return stream_util.ThreadSwitchingConsumer(request_consumer, self._pool)
+
+
+class _UnaryConsumer(stream.Consumer):
+ """A Consumer that only allows consumption of exactly one value."""
+
+ def __init__(self, action):
+ self._lock = threading.Lock()
+ self._action = action
+ self._consumed = False
+ self._terminated = False
+
+ def consume(self, value):
+ with self._lock:
+ if self._consumed:
+ raise ValueError('Unary consumer already consumed!')
+ elif self._terminated:
+ raise ValueError('Unary consumer already terminated!')
+ else:
+ self._consumed = True
+
+ self._action(value)
+
+ def terminate(self):
+ with self._lock:
+ if not self._consumed:
+ raise ValueError('Unary consumer hasn\'t yet consumed!')
+ elif self._terminated:
+ raise ValueError('Unary consumer already terminated!')
+ else:
+ self._terminated = True
+
+ def consume_and_terminate(self, value):
+ with self._lock:
+ if self._consumed:
+ raise ValueError('Unary consumer already consumed!')
+ elif self._terminated:
+ raise ValueError('Unary consumer already terminated!')
+ else:
+ self._consumed = True
+ self._terminated = True
+
+ self._action(value)
+
+
+class _UnaryUnaryAdaptation(object):
+
+ def __init__(self, unary_unary_test_method):
+ self._method = unary_unary_test_method
+
+ def service(self, response_consumer, context, control):
+ def action(request):
+ self._method.service(
+ request, response_consumer.consume_and_terminate, context, control)
+ return _UnaryConsumer(action)
+
+
+class _UnaryStreamAdaptation(object):
+
+ def __init__(self, unary_stream_test_method):
+ self._method = unary_stream_test_method
+
+ def service(self, response_consumer, context, control):
+ def action(request):
+ self._method.service(request, response_consumer, context, control)
+ return _UnaryConsumer(action)
+
+
+class _StreamUnaryAdaptation(object):
+
+ def __init__(self, stream_unary_test_method):
+ self._method = stream_unary_test_method
+
+ def service(self, response_consumer, context, control):
+ return self._method.service(
+ response_consumer.consume_and_terminate, context, control)
+
+
+class _MultiMethodImplementation(face.MultiMethodImplementation):
+
+ def __init__(self, methods, control, pool):
+ self._methods = methods
+ self._control = control
+ self._pool = pool
+
+ def service(self, group, name, response_consumer, context):
+ method = self._methods.get(group, name, None)
+ if method is None:
+ raise face.NoSuchMethodError(group, name)
+ elif self._pool is None:
+ return method(response_consumer, context, self._control)
+ else:
+ request_consumer = method(response_consumer, context, self._control)
+ return stream_util.ThreadSwitchingConsumer(request_consumer, self._pool)
+
+
+class _Assembly(
+ collections.namedtuple(
+ '_Assembly',
+ ['methods', 'inlines', 'events', 'adaptations', 'messages'])):
+ """An intermediate structure created when creating a TestServiceDigest."""
+
+
+def _assemble(
+ scenarios, identifiers, inline_method_constructor, event_method_constructor,
+ adapter, control, pool):
+ """Creates an _Assembly from the given scenarios."""
+ methods = {}
+ inlines = {}
+ events = {}
+ adaptations = {}
+ messages = {}
+ for identifier, scenario in scenarios.iteritems():
+ if identifier in identifiers:
+ raise ValueError('Repeated identifier "(%s, %s)"!' % identifier)
+
+ test_method = scenario[0]
+ inline_method = inline_method_constructor(test_method, control)
+ event_method = event_method_constructor(test_method, control, pool)
+ adaptation = adapter(test_method)
+
+ methods[identifier] = test_method
+ inlines[identifier] = inline_method
+ events[identifier] = event_method
+ adaptations[identifier] = adaptation
+ messages[identifier] = scenario[1]
+
+ return _Assembly(methods, inlines, events, adaptations, messages)
+
+
+def digest(service, control, pool):
+ """Creates a TestServiceDigest from a TestService.
+
+ Args:
+ service: A _service.TestService.
+ control: A test_control.Control.
+ pool: If RPC methods should be serviced in a separate thread, a thread pool.
+ None if RPC methods should be serviced in the thread belonging to the
+ run-time that calls for their service.
+
+ Returns:
+ A TestServiceDigest synthesized from the given service.TestService.
+ """
+ identifiers = set()
+
+ unary_unary = _assemble(
+ service.unary_unary_scenarios(), identifiers, _InlineUnaryUnaryMethod,
+ _EventUnaryUnaryMethod, _UnaryUnaryAdaptation, control, pool)
+ identifiers.update(unary_unary.inlines)
+
+ unary_stream = _assemble(
+ service.unary_stream_scenarios(), identifiers, _InlineUnaryStreamMethod,
+ _EventUnaryStreamMethod, _UnaryStreamAdaptation, control, pool)
+ identifiers.update(unary_stream.inlines)
+
+ stream_unary = _assemble(
+ service.stream_unary_scenarios(), identifiers, _InlineStreamUnaryMethod,
+ _EventStreamUnaryMethod, _StreamUnaryAdaptation, control, pool)
+ identifiers.update(stream_unary.inlines)
+
+ stream_stream = _assemble(
+ service.stream_stream_scenarios(), identifiers, _InlineStreamStreamMethod,
+ _EventStreamStreamMethod, _IDENTITY, control, pool)
+ identifiers.update(stream_stream.inlines)
+
+ methods = dict(unary_unary.methods)
+ methods.update(unary_stream.methods)
+ methods.update(stream_unary.methods)
+ methods.update(stream_stream.methods)
+ adaptations = dict(unary_unary.adaptations)
+ adaptations.update(unary_stream.adaptations)
+ adaptations.update(stream_unary.adaptations)
+ adaptations.update(stream_stream.adaptations)
+ inlines = dict(unary_unary.inlines)
+ inlines.update(unary_stream.inlines)
+ inlines.update(stream_unary.inlines)
+ inlines.update(stream_stream.inlines)
+ events = dict(unary_unary.events)
+ events.update(unary_stream.events)
+ events.update(stream_unary.events)
+ events.update(stream_stream.events)
+
+ return TestServiceDigest(
+ methods,
+ inlines,
+ events,
+ _MultiMethodImplementation(adaptations, control, pool),
+ unary_unary.messages,
+ unary_stream.messages,
+ stream_unary.messages,
+ stream_stream.messages)
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_event_invocation_synchronous_event_service.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_event_invocation_synchronous_event_service.py
new file mode 100644
index 0000000000..34db6c3e55
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_event_invocation_synchronous_event_service.py
@@ -0,0 +1,381 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Test code for the Face layer of RPC Framework."""
+
+import abc
+import unittest
+
+# test_interfaces is referenced from specification in this module.
+from grpc.framework.interfaces.face import face
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.common import test_control
+from tests.unit.framework.common import test_coverage
+from tests.unit.framework.interfaces.face import _3069_test_constant
+from tests.unit.framework.interfaces.face import _digest
+from tests.unit.framework.interfaces.face import _receiver
+from tests.unit.framework.interfaces.face import _stock_service
+from tests.unit.framework.interfaces.face import test_interfaces # pylint: disable=unused-import
+
+
+class TestCase(test_coverage.Coverage, unittest.TestCase):
+ """A test of the Face layer of RPC Framework.
+
+ Concrete subclasses must have an "implementation" attribute of type
+ test_interfaces.Implementation and an "invoker_constructor" attribute of type
+ _invocation.InvokerConstructor.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ NAME = 'EventInvocationSynchronousEventServiceTest'
+
+ def setUp(self):
+ """See unittest.TestCase.setUp for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self._control = test_control.PauseFailControl()
+ self._digest = _digest.digest(
+ _stock_service.STOCK_TEST_SERVICE, self._control, None)
+
+ generic_stub, dynamic_stubs, self._memo = self.implementation.instantiate(
+ self._digest.methods, self._digest.event_method_implementations, None)
+ self._invoker = self.invoker_constructor.construct_invoker(
+ generic_stub, dynamic_stubs, self._digest.methods)
+
+ def tearDown(self):
+ """See unittest.TestCase.tearDown for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self._invoker = None
+ self.implementation.destantiate(self._memo)
+
+ def testSuccessfulUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ receiver = _receiver.Receiver()
+
+ self._invoker.event(group, method)(
+ request, receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ receiver.block_until_terminated()
+ response = receiver.unary_response()
+
+ test_messages.verify(request, response, self)
+
+ def testSuccessfulUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ receiver = _receiver.Receiver()
+
+ self._invoker.event(group, method)(
+ request, receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ receiver.block_until_terminated()
+ responses = receiver.stream_responses()
+
+ test_messages.verify(request, responses, self)
+
+ def testSuccessfulStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ receiver = _receiver.Receiver()
+
+ call_consumer = self._invoker.event(group, method)(
+ receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ for request in requests:
+ call_consumer.consume(request)
+ call_consumer.terminate()
+ receiver.block_until_terminated()
+ response = receiver.unary_response()
+
+ test_messages.verify(requests, response, self)
+
+ def testSuccessfulStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ receiver = _receiver.Receiver()
+
+ call_consumer = self._invoker.event(group, method)(
+ receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ for request in requests:
+ call_consumer.consume(request)
+ call_consumer.terminate()
+ receiver.block_until_terminated()
+ responses = receiver.stream_responses()
+
+ test_messages.verify(requests, responses, self)
+
+ def testSequentialInvocations(self):
+ # pylint: disable=cell-var-from-loop
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ second_request = test_messages.request()
+ second_receiver = _receiver.Receiver()
+
+ def make_second_invocation():
+ self._invoker.event(group, method)(
+ second_request, second_receiver, second_receiver.abort,
+ test_constants.LONG_TIMEOUT)
+
+ class FirstReceiver(_receiver.Receiver):
+
+ def complete(self, terminal_metadata, code, details):
+ super(FirstReceiver, self).complete(
+ terminal_metadata, code, details)
+ make_second_invocation()
+
+ first_receiver = FirstReceiver()
+
+ self._invoker.event(group, method)(
+ first_request, first_receiver, first_receiver.abort,
+ test_constants.LONG_TIMEOUT)
+ second_receiver.block_until_terminated()
+
+ first_response = first_receiver.unary_response()
+ second_response = second_receiver.unary_response()
+ test_messages.verify(first_request, first_response, self)
+ test_messages.verify(second_request, second_response, self)
+
+ def testParallelInvocations(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ first_receiver = _receiver.Receiver()
+ second_request = test_messages.request()
+ second_receiver = _receiver.Receiver()
+
+ self._invoker.event(group, method)(
+ first_request, first_receiver, first_receiver.abort,
+ test_constants.LONG_TIMEOUT)
+ self._invoker.event(group, method)(
+ second_request, second_receiver, second_receiver.abort,
+ test_constants.LONG_TIMEOUT)
+ first_receiver.block_until_terminated()
+ second_receiver.block_until_terminated()
+
+ first_response = first_receiver.unary_response()
+ second_response = second_receiver.unary_response()
+ test_messages.verify(first_request, first_response, self)
+ test_messages.verify(second_request, second_response, self)
+
+ @unittest.skip('TODO(nathaniel): implement.')
+ def testWaitingForSomeButNotAllParallelInvocations(self):
+ raise NotImplementedError()
+
+ def testCancelledUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ receiver = _receiver.Receiver()
+
+ with self._control.pause():
+ call = self._invoker.event(group, method)(
+ request, receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ call.cancel()
+ receiver.block_until_terminated()
+
+ self.assertIs(face.Abortion.Kind.CANCELLED, receiver.abortion().kind)
+
+ def testCancelledUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ receiver = _receiver.Receiver()
+
+ call = self._invoker.event(group, method)(
+ request, receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ call.cancel()
+ receiver.block_until_terminated()
+
+ self.assertIs(face.Abortion.Kind.CANCELLED, receiver.abortion().kind)
+
+ def testCancelledStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ receiver = _receiver.Receiver()
+
+ call_consumer = self._invoker.event(group, method)(
+ receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ for request in requests:
+ call_consumer.consume(request)
+ call_consumer.cancel()
+ receiver.block_until_terminated()
+
+ self.assertIs(face.Abortion.Kind.CANCELLED, receiver.abortion().kind)
+
+ def testCancelledStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for unused_test_messages in test_messages_sequence:
+ receiver = _receiver.Receiver()
+
+ call_consumer = self._invoker.event(group, method)(
+ receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ call_consumer.cancel()
+ receiver.block_until_terminated()
+
+ self.assertIs(face.Abortion.Kind.CANCELLED, receiver.abortion().kind)
+
+ def testExpiredUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ receiver = _receiver.Receiver()
+
+ with self._control.pause():
+ self._invoker.event(group, method)(
+ request, receiver, receiver.abort,
+ _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ receiver.block_until_terminated()
+
+ self.assertIs(face.Abortion.Kind.EXPIRED, receiver.abortion().kind)
+
+ def testExpiredUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ receiver = _receiver.Receiver()
+
+ with self._control.pause():
+ self._invoker.event(group, method)(
+ request, receiver, receiver.abort,
+ _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ receiver.block_until_terminated()
+
+ self.assertIs(face.Abortion.Kind.EXPIRED, receiver.abortion().kind)
+
+ def testExpiredStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for unused_test_messages in test_messages_sequence:
+ receiver = _receiver.Receiver()
+
+ self._invoker.event(group, method)(
+ receiver, receiver.abort, _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ receiver.block_until_terminated()
+
+ self.assertIs(face.Abortion.Kind.EXPIRED, receiver.abortion().kind)
+
+ def testExpiredStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ receiver = _receiver.Receiver()
+
+ call_consumer = self._invoker.event(group, method)(
+ receiver, receiver.abort, _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ for request in requests:
+ call_consumer.consume(request)
+ receiver.block_until_terminated()
+
+ self.assertIs(face.Abortion.Kind.EXPIRED, receiver.abortion().kind)
+
+ def testFailedUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ receiver = _receiver.Receiver()
+
+ with self._control.fail():
+ self._invoker.event(group, method)(
+ request, receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ receiver.block_until_terminated()
+
+ self.assertIs(
+ face.Abortion.Kind.REMOTE_FAILURE, receiver.abortion().kind)
+
+ def testFailedUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ receiver = _receiver.Receiver()
+
+ with self._control.fail():
+ self._invoker.event(group, method)(
+ request, receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ receiver.block_until_terminated()
+
+ self.assertIs(
+ face.Abortion.Kind.REMOTE_FAILURE, receiver.abortion().kind)
+
+ def testFailedStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ receiver = _receiver.Receiver()
+
+ with self._control.fail():
+ call_consumer = self._invoker.event(group, method)(
+ receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ for request in requests:
+ call_consumer.consume(request)
+ call_consumer.terminate()
+ receiver.block_until_terminated()
+
+ self.assertIs(
+ face.Abortion.Kind.REMOTE_FAILURE, receiver.abortion().kind)
+
+ def testFailedStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ receiver = _receiver.Receiver()
+
+ with self._control.fail():
+ call_consumer = self._invoker.event(group, method)(
+ receiver, receiver.abort, test_constants.LONG_TIMEOUT)
+ for request in requests:
+ call_consumer.consume(request)
+ call_consumer.terminate()
+ receiver.block_until_terminated()
+
+ self.assertIs(
+ face.Abortion.Kind.REMOTE_FAILURE, receiver.abortion().kind)
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_future_invocation_asynchronous_event_service.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_future_invocation_asynchronous_event_service.py
new file mode 100644
index 0000000000..c178f2f108
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_future_invocation_asynchronous_event_service.py
@@ -0,0 +1,435 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Test code for the Face layer of RPC Framework."""
+
+import abc
+import contextlib
+import threading
+import unittest
+
+# test_interfaces is referenced from specification in this module.
+from grpc.framework.foundation import logging_pool
+from grpc.framework.interfaces.face import face
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.common import test_control
+from tests.unit.framework.common import test_coverage
+from tests.unit.framework.interfaces.face import _3069_test_constant
+from tests.unit.framework.interfaces.face import _digest
+from tests.unit.framework.interfaces.face import _stock_service
+from tests.unit.framework.interfaces.face import test_interfaces # pylint: disable=unused-import
+
+
+class _PauseableIterator(object):
+
+ def __init__(self, upstream):
+ self._upstream = upstream
+ self._condition = threading.Condition()
+ self._paused = False
+
+ @contextlib.contextmanager
+ def pause(self):
+ with self._condition:
+ self._paused = True
+ yield
+ with self._condition:
+ self._paused = False
+ self._condition.notify_all()
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ with self._condition:
+ while self._paused:
+ self._condition.wait()
+ return next(self._upstream)
+
+
+class _Callback(object):
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._called = False
+ self._passed_future = None
+ self._passed_other_stuff = None
+
+ def __call__(self, *args, **kwargs):
+ with self._condition:
+ self._called = True
+ if args:
+ self._passed_future = args[0]
+ if 1 < len(args) or kwargs:
+ self._passed_other_stuff = tuple(args[1:]), dict(kwargs)
+ self._condition.notify_all()
+
+ def future(self):
+ with self._condition:
+ while True:
+ if self._passed_other_stuff is not None:
+ raise ValueError(
+ 'Test callback passed unexpected values: %s',
+ self._passed_other_stuff)
+ elif self._called:
+ return self._passed_future
+ else:
+ self._condition.wait()
+
+
+class TestCase(test_coverage.Coverage, unittest.TestCase):
+ """A test of the Face layer of RPC Framework.
+
+ Concrete subclasses must have an "implementation" attribute of type
+ test_interfaces.Implementation and an "invoker_constructor" attribute of type
+ _invocation.InvokerConstructor.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ NAME = 'FutureInvocationAsynchronousEventServiceTest'
+
+ def setUp(self):
+ """See unittest.TestCase.setUp for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self._control = test_control.PauseFailControl()
+ self._digest_pool = logging_pool.pool(test_constants.POOL_SIZE)
+ self._digest = _digest.digest(
+ _stock_service.STOCK_TEST_SERVICE, self._control, self._digest_pool)
+
+ generic_stub, dynamic_stubs, self._memo = self.implementation.instantiate(
+ self._digest.methods, self._digest.event_method_implementations, None)
+ self._invoker = self.invoker_constructor.construct_invoker(
+ generic_stub, dynamic_stubs, self._digest.methods)
+
+ def tearDown(self):
+ """See unittest.TestCase.tearDown for full specification.
+
+ Overriding implementations must call this implementation.
+ """
+ self._invoker = None
+ self.implementation.destantiate(self._memo)
+ self._digest_pool.shutdown(wait=True)
+
+ def testSuccessfulUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = _Callback()
+
+ response_future = self._invoker.future(group, method)(
+ request, test_constants.LONG_TIMEOUT)
+ response_future.add_done_callback(callback)
+ response = response_future.result()
+
+ test_messages.verify(request, response, self)
+ self.assertIs(callback.future(), response_future)
+
+ def testSuccessfulUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ response_iterator = self._invoker.future(group, method)(
+ request, test_constants.LONG_TIMEOUT)
+ responses = list(response_iterator)
+
+ test_messages.verify(request, responses, self)
+
+ def testSuccessfulStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ request_iterator = _PauseableIterator(iter(requests))
+ callback = _Callback()
+
+ # Use of a paused iterator of requests allows us to test that control is
+ # returned to calling code before the iterator yields any requests.
+ with request_iterator.pause():
+ response_future = self._invoker.future(group, method)(
+ request_iterator, test_constants.LONG_TIMEOUT)
+ response_future.add_done_callback(callback)
+ future_passed_to_callback = callback.future()
+ response = future_passed_to_callback.result()
+
+ test_messages.verify(requests, response, self)
+ self.assertIs(future_passed_to_callback, response_future)
+
+ def testSuccessfulStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ request_iterator = _PauseableIterator(iter(requests))
+
+ # Use of a paused iterator of requests allows us to test that control is
+ # returned to calling code before the iterator yields any requests.
+ with request_iterator.pause():
+ response_iterator = self._invoker.future(group, method)(
+ request_iterator, test_constants.LONG_TIMEOUT)
+ responses = list(response_iterator)
+
+ test_messages.verify(requests, responses, self)
+
+ def testSequentialInvocations(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ second_request = test_messages.request()
+
+ first_response_future = self._invoker.future(group, method)(
+ first_request, test_constants.LONG_TIMEOUT)
+ first_response = first_response_future.result()
+
+ test_messages.verify(first_request, first_response, self)
+
+ second_response_future = self._invoker.future(group, method)(
+ second_request, test_constants.LONG_TIMEOUT)
+ second_response = second_response_future.result()
+
+ test_messages.verify(second_request, second_response, self)
+
+ def testParallelInvocations(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ first_request = test_messages.request()
+ second_request = test_messages.request()
+
+ first_response_future = self._invoker.future(group, method)(
+ first_request, test_constants.LONG_TIMEOUT)
+ second_response_future = self._invoker.future(group, method)(
+ second_request, test_constants.LONG_TIMEOUT)
+ first_response = first_response_future.result()
+ second_response = second_response_future.result()
+
+ test_messages.verify(first_request, first_response, self)
+ test_messages.verify(second_request, second_response, self)
+
+ @unittest.skip('TODO(nathaniel): implement.')
+ def testWaitingForSomeButNotAllParallelInvocations(self):
+ raise NotImplementedError()
+
+ def testCancelledUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = _Callback()
+
+ with self._control.pause():
+ response_future = self._invoker.future(group, method)(
+ request, test_constants.LONG_TIMEOUT)
+ response_future.add_done_callback(callback)
+ cancel_method_return_value = response_future.cancel()
+
+ self.assertIs(callback.future(), response_future)
+ self.assertFalse(cancel_method_return_value)
+ self.assertTrue(response_future.cancelled())
+
+ def testCancelledUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self._control.pause():
+ response_iterator = self._invoker.future(group, method)(
+ request, test_constants.LONG_TIMEOUT)
+ response_iterator.cancel()
+
+ with self.assertRaises(face.CancellationError):
+ next(response_iterator)
+
+ def testCancelledStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = _Callback()
+
+ with self._control.pause():
+ response_future = self._invoker.future(group, method)(
+ iter(requests), test_constants.LONG_TIMEOUT)
+ response_future.add_done_callback(callback)
+ cancel_method_return_value = response_future.cancel()
+
+ self.assertIs(callback.future(), response_future)
+ self.assertFalse(cancel_method_return_value)
+ self.assertTrue(response_future.cancelled())
+
+ def testCancelledStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self._control.pause():
+ response_iterator = self._invoker.future(group, method)(
+ iter(requests), test_constants.LONG_TIMEOUT)
+ response_iterator.cancel()
+
+ with self.assertRaises(face.CancellationError):
+ next(response_iterator)
+
+ def testExpiredUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = _Callback()
+
+ with self._control.pause():
+ response_future = self._invoker.future(
+ group, method)(request, _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ response_future.add_done_callback(callback)
+ self.assertIs(callback.future(), response_future)
+ self.assertIsInstance(
+ response_future.exception(), face.ExpirationError)
+ with self.assertRaises(face.ExpirationError):
+ response_future.result()
+
+ def testExpiredUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ with self._control.pause():
+ response_iterator = self._invoker.future(group, method)(
+ request, _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ with self.assertRaises(face.ExpirationError):
+ list(response_iterator)
+
+ def testExpiredStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = _Callback()
+
+ with self._control.pause():
+ response_future = self._invoker.future(group, method)(
+ iter(requests), _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ response_future.add_done_callback(callback)
+ self.assertIs(callback.future(), response_future)
+ self.assertIsInstance(
+ response_future.exception(), face.ExpirationError)
+ with self.assertRaises(face.ExpirationError):
+ response_future.result()
+
+ def testExpiredStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ with self._control.pause():
+ response_iterator = self._invoker.future(group, method)(
+ iter(requests), _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ with self.assertRaises(face.ExpirationError):
+ list(response_iterator)
+
+ def testFailedUnaryRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+ callback = _Callback()
+
+ with self._control.fail():
+ response_future = self._invoker.future(group, method)(
+ request, _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ response_future.add_done_callback(callback)
+
+ self.assertIs(callback.future(), response_future)
+ # Because the servicer fails outside of the thread from which the
+ # servicer-side runtime called into it its failure is
+ # indistinguishable from simply not having called its
+ # response_callback before the expiration of the RPC.
+ self.assertIsInstance(
+ response_future.exception(), face.ExpirationError)
+ with self.assertRaises(face.ExpirationError):
+ response_future.result()
+
+ def testFailedUnaryRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.unary_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ request = test_messages.request()
+
+ # Because the servicer fails outside of the thread from which the
+ # servicer-side runtime called into it its failure is indistinguishable
+ # from simply not having called its response_consumer before the
+ # expiration of the RPC.
+ with self._control.fail(), self.assertRaises(face.ExpirationError):
+ response_iterator = self._invoker.future(group, method)(
+ request, _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ list(response_iterator)
+
+ def testFailedStreamRequestUnaryResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_unary_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+ callback = _Callback()
+
+ with self._control.fail():
+ response_future = self._invoker.future(group, method)(
+ iter(requests), _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ response_future.add_done_callback(callback)
+
+ self.assertIs(callback.future(), response_future)
+ # Because the servicer fails outside of the thread from which the
+ # servicer-side runtime called into it its failure is
+ # indistinguishable from simply not having called its
+ # response_callback before the expiration of the RPC.
+ self.assertIsInstance(
+ response_future.exception(), face.ExpirationError)
+ with self.assertRaises(face.ExpirationError):
+ response_future.result()
+
+ def testFailedStreamRequestStreamResponse(self):
+ for (group, method), test_messages_sequence in (
+ self._digest.stream_stream_messages_sequences.iteritems()):
+ for test_messages in test_messages_sequence:
+ requests = test_messages.requests()
+
+ # Because the servicer fails outside of the thread from which the
+ # servicer-side runtime called into it its failure is indistinguishable
+ # from simply not having called its response_consumer before the
+ # expiration of the RPC.
+ with self._control.fail(), self.assertRaises(face.ExpirationError):
+ response_iterator = self._invoker.future(group, method)(
+ iter(requests), _3069_test_constant.REALLY_SHORT_TIMEOUT)
+ list(response_iterator)
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_invocation.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_invocation.py
new file mode 100644
index 0000000000..448e845a08
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_invocation.py
@@ -0,0 +1,213 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Coverage across the Face layer's generic-to-dynamic range for invocation."""
+
+import abc
+
+from grpc.framework.common import cardinality
+
+_CARDINALITY_TO_GENERIC_BLOCKING_BEHAVIOR = {
+ cardinality.Cardinality.UNARY_UNARY: 'blocking_unary_unary',
+ cardinality.Cardinality.UNARY_STREAM: 'inline_unary_stream',
+ cardinality.Cardinality.STREAM_UNARY: 'blocking_stream_unary',
+ cardinality.Cardinality.STREAM_STREAM: 'inline_stream_stream',
+}
+
+_CARDINALITY_TO_GENERIC_FUTURE_BEHAVIOR = {
+ cardinality.Cardinality.UNARY_UNARY: 'future_unary_unary',
+ cardinality.Cardinality.UNARY_STREAM: 'inline_unary_stream',
+ cardinality.Cardinality.STREAM_UNARY: 'future_stream_unary',
+ cardinality.Cardinality.STREAM_STREAM: 'inline_stream_stream',
+}
+
+_CARDINALITY_TO_GENERIC_EVENT_BEHAVIOR = {
+ cardinality.Cardinality.UNARY_UNARY: 'event_unary_unary',
+ cardinality.Cardinality.UNARY_STREAM: 'event_unary_stream',
+ cardinality.Cardinality.STREAM_UNARY: 'event_stream_unary',
+ cardinality.Cardinality.STREAM_STREAM: 'event_stream_stream',
+}
+
+_CARDINALITY_TO_MULTI_CALLABLE_ATTRIBUTE = {
+ cardinality.Cardinality.UNARY_UNARY: 'unary_unary',
+ cardinality.Cardinality.UNARY_STREAM: 'unary_stream',
+ cardinality.Cardinality.STREAM_UNARY: 'stream_unary',
+ cardinality.Cardinality.STREAM_STREAM: 'stream_stream',
+}
+
+
+class Invoker(object):
+ """A type used to invoke test RPCs."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def blocking(self, group, name):
+ """Invokes an RPC with blocking control flow."""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def future(self, group, name):
+ """Invokes an RPC with future control flow."""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def event(self, group, name):
+ """Invokes an RPC with event control flow."""
+ raise NotImplementedError()
+
+
+class InvokerConstructor(object):
+ """A type used to create Invokers."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def name(self):
+ """Specifies the name of the Invoker constructed by this object."""
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def construct_invoker(self, generic_stub, dynamic_stubs, methods):
+ """Constructs an Invoker for the given stubs and methods."""
+ raise NotImplementedError()
+
+
+class _GenericInvoker(Invoker):
+
+ def __init__(self, generic_stub, methods):
+ self._stub = generic_stub
+ self._methods = methods
+
+ def _behavior(self, group, name, cardinality_to_generic_method):
+ method_cardinality = self._methods[group, name].cardinality()
+ behavior = getattr(
+ self._stub, cardinality_to_generic_method[method_cardinality])
+ return lambda *args, **kwargs: behavior(group, name, *args, **kwargs)
+
+ def blocking(self, group, name):
+ return self._behavior(
+ group, name, _CARDINALITY_TO_GENERIC_BLOCKING_BEHAVIOR)
+
+ def future(self, group, name):
+ return self._behavior(group, name, _CARDINALITY_TO_GENERIC_FUTURE_BEHAVIOR)
+
+ def event(self, group, name):
+ return self._behavior(group, name, _CARDINALITY_TO_GENERIC_EVENT_BEHAVIOR)
+
+
+class _GenericInvokerConstructor(InvokerConstructor):
+
+ def name(self):
+ return 'GenericInvoker'
+
+ def construct_invoker(self, generic_stub, dynamic_stub, methods):
+ return _GenericInvoker(generic_stub, methods)
+
+
+class _MultiCallableInvoker(Invoker):
+
+ def __init__(self, generic_stub, methods):
+ self._stub = generic_stub
+ self._methods = methods
+
+ def _multi_callable(self, group, name):
+ method_cardinality = self._methods[group, name].cardinality()
+ behavior = getattr(
+ self._stub,
+ _CARDINALITY_TO_MULTI_CALLABLE_ATTRIBUTE[method_cardinality])
+ return behavior(group, name)
+
+ def blocking(self, group, name):
+ return self._multi_callable(group, name)
+
+ def future(self, group, name):
+ method_cardinality = self._methods[group, name].cardinality()
+ behavior = getattr(
+ self._stub,
+ _CARDINALITY_TO_MULTI_CALLABLE_ATTRIBUTE[method_cardinality])
+ if method_cardinality in (
+ cardinality.Cardinality.UNARY_UNARY,
+ cardinality.Cardinality.STREAM_UNARY):
+ return behavior(group, name).future
+ else:
+ return behavior(group, name)
+
+ def event(self, group, name):
+ return self._multi_callable(group, name).event
+
+
+class _MultiCallableInvokerConstructor(InvokerConstructor):
+
+ def name(self):
+ return 'MultiCallableInvoker'
+
+ def construct_invoker(self, generic_stub, dynamic_stub, methods):
+ return _MultiCallableInvoker(generic_stub, methods)
+
+
+class _DynamicInvoker(Invoker):
+
+ def __init__(self, dynamic_stubs, methods):
+ self._stubs = dynamic_stubs
+ self._methods = methods
+
+ def blocking(self, group, name):
+ return getattr(self._stubs[group], name)
+
+ def future(self, group, name):
+ if self._methods[group, name].cardinality() in (
+ cardinality.Cardinality.UNARY_UNARY,
+ cardinality.Cardinality.STREAM_UNARY):
+ return getattr(self._stubs[group], name).future
+ else:
+ return getattr(self._stubs[group], name)
+
+ def event(self, group, name):
+ return getattr(self._stubs[group], name).event
+
+
+class _DynamicInvokerConstructor(InvokerConstructor):
+
+ def name(self):
+ return 'DynamicInvoker'
+
+ def construct_invoker(self, generic_stub, dynamic_stubs, methods):
+ return _DynamicInvoker(dynamic_stubs, methods)
+
+
+def invoker_constructors():
+ """Creates a sequence of InvokerConstructors to use in tests of RPCs.
+
+ Returns:
+ A sequence of InvokerConstructors.
+ """
+ return (
+ _GenericInvokerConstructor(),
+ _MultiCallableInvokerConstructor(),
+ _DynamicInvokerConstructor(),
+ )
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_receiver.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_receiver.py
new file mode 100644
index 0000000000..2e444ff09d
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_receiver.py
@@ -0,0 +1,95 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""A utility useful in tests of asynchronous, event-driven interfaces."""
+
+import threading
+
+from grpc.framework.interfaces.face import face
+
+
+class Receiver(face.ResponseReceiver):
+ """A utility object useful in tests of asynchronous code."""
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._initial_metadata = None
+ self._responses = []
+ self._terminal_metadata = None
+ self._code = None
+ self._details = None
+ self._completed = False
+ self._abortion = None
+
+ def abort(self, abortion):
+ with self._condition:
+ self._abortion = abortion
+ self._condition.notify_all()
+
+ def initial_metadata(self, initial_metadata):
+ with self._condition:
+ self._initial_metadata = initial_metadata
+
+ def response(self, response):
+ with self._condition:
+ self._responses.append(response)
+
+ def complete(self, terminal_metadata, code, details):
+ with self._condition:
+ self._terminal_metadata = terminal_metadata
+ self._code = code
+ self._details = details
+ self._completed = True
+ self._condition.notify_all()
+
+ def block_until_terminated(self):
+ with self._condition:
+ while self._abortion is None and not self._completed:
+ self._condition.wait()
+
+ def unary_response(self):
+ with self._condition:
+ if self._abortion is not None:
+ raise AssertionError('Aborted with abortion "%s"!' % self._abortion)
+ elif len(self._responses) != 1:
+ raise AssertionError(
+ '%d responses received, not exactly one!', len(self._responses))
+ else:
+ return self._responses[0]
+
+ def stream_responses(self):
+ with self._condition:
+ if self._abortion is None:
+ return list(self._responses)
+ else:
+ raise AssertionError('Aborted with abortion "%s"!' % self._abortion)
+
+ def abortion(self):
+ with self._condition:
+ return self._abortion
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_service.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_service.py
new file mode 100644
index 0000000000..28941e2ad0
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_service.py
@@ -0,0 +1,332 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Private interfaces implemented by data sets used in Face-layer tests."""
+
+import abc
+
+# face is referenced from specification in this module.
+from grpc.framework.interfaces.face import face # pylint: disable=unused-import
+from tests.unit.framework.interfaces.face import test_interfaces
+
+
+class UnaryUnaryTestMethodImplementation(test_interfaces.Method):
+ """A controllable implementation of a unary-unary method."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def service(self, request, response_callback, context, control):
+ """Services an RPC that accepts one message and produces one message.
+
+ Args:
+ request: The single request message for the RPC.
+ response_callback: A callback to be called to accept the response message
+ of the RPC.
+ context: An face.ServicerContext object.
+ control: A test_control.Control to control execution of this method.
+
+ Raises:
+ abandonment.Abandoned: May or may not be raised when the RPC has been
+ aborted.
+ """
+ raise NotImplementedError()
+
+
+class UnaryUnaryTestMessages(object):
+ """A type for unary-request-unary-response message pairings."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def request(self):
+ """Affords a request message.
+
+ Implementations of this method should return a different message with each
+ call so that multiple test executions of the test method may be made with
+ different inputs.
+
+ Returns:
+ A request message.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify(self, request, response, test_case):
+ """Verifies that the computed response matches the given request.
+
+ Args:
+ request: A request message.
+ response: A response message.
+ test_case: A unittest.TestCase object affording useful assertion methods.
+
+ Raises:
+ AssertionError: If the request and response do not match, indicating that
+ there was some problem executing the RPC under test.
+ """
+ raise NotImplementedError()
+
+
+class UnaryStreamTestMethodImplementation(test_interfaces.Method):
+ """A controllable implementation of a unary-stream method."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def service(self, request, response_consumer, context, control):
+ """Services an RPC that takes one message and produces a stream of messages.
+
+ Args:
+ request: The single request message for the RPC.
+ response_consumer: A stream.Consumer to be called to accept the response
+ messages of the RPC.
+ context: A face.ServicerContext object.
+ control: A test_control.Control to control execution of this method.
+
+ Raises:
+ abandonment.Abandoned: May or may not be raised when the RPC has been
+ aborted.
+ """
+ raise NotImplementedError()
+
+
+class UnaryStreamTestMessages(object):
+ """A type for unary-request-stream-response message pairings."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def request(self):
+ """Affords a request message.
+
+ Implementations of this method should return a different message with each
+ call so that multiple test executions of the test method may be made with
+ different inputs.
+
+ Returns:
+ A request message.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify(self, request, responses, test_case):
+ """Verifies that the computed responses match the given request.
+
+ Args:
+ request: A request message.
+ responses: A sequence of response messages.
+ test_case: A unittest.TestCase object affording useful assertion methods.
+
+ Raises:
+ AssertionError: If the request and responses do not match, indicating that
+ there was some problem executing the RPC under test.
+ """
+ raise NotImplementedError()
+
+
+class StreamUnaryTestMethodImplementation(test_interfaces.Method):
+ """A controllable implementation of a stream-unary method."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def service(self, response_callback, context, control):
+ """Services an RPC that takes a stream of messages and produces one message.
+
+ Args:
+ response_callback: A callback to be called to accept the response message
+ of the RPC.
+ context: A face.ServicerContext object.
+ control: A test_control.Control to control execution of this method.
+
+ Returns:
+ A stream.Consumer with which to accept the request messages of the RPC.
+ The consumer returned from this method may or may not be invoked to
+ completion: in the case of RPC abortion, RPC Framework will simply stop
+ passing messages to this object. Implementations must not assume that
+ this object will be called to completion of the request stream or even
+ called at all.
+
+ Raises:
+ abandonment.Abandoned: May or may not be raised when the RPC has been
+ aborted.
+ """
+ raise NotImplementedError()
+
+
+class StreamUnaryTestMessages(object):
+ """A type for stream-request-unary-response message pairings."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def requests(self):
+ """Affords a sequence of request messages.
+
+ Implementations of this method should return a different sequences with each
+ call so that multiple test executions of the test method may be made with
+ different inputs.
+
+ Returns:
+ A sequence of request messages.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify(self, requests, response, test_case):
+ """Verifies that the computed response matches the given requests.
+
+ Args:
+ requests: A sequence of request messages.
+ response: A response message.
+ test_case: A unittest.TestCase object affording useful assertion methods.
+
+ Raises:
+ AssertionError: If the requests and response do not match, indicating that
+ there was some problem executing the RPC under test.
+ """
+ raise NotImplementedError()
+
+
+class StreamStreamTestMethodImplementation(test_interfaces.Method):
+ """A controllable implementation of a stream-stream method."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def service(self, response_consumer, context, control):
+ """Services an RPC that accepts and produces streams of messages.
+
+ Args:
+ response_consumer: A stream.Consumer to be called to accept the response
+ messages of the RPC.
+ context: A face.ServicerContext object.
+ control: A test_control.Control to control execution of this method.
+
+ Returns:
+ A stream.Consumer with which to accept the request messages of the RPC.
+ The consumer returned from this method may or may not be invoked to
+ completion: in the case of RPC abortion, RPC Framework will simply stop
+ passing messages to this object. Implementations must not assume that
+ this object will be called to completion of the request stream or even
+ called at all.
+
+ Raises:
+ abandonment.Abandoned: May or may not be raised when the RPC has been
+ aborted.
+ """
+ raise NotImplementedError()
+
+
+class StreamStreamTestMessages(object):
+ """A type for stream-request-stream-response message pairings."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def requests(self):
+ """Affords a sequence of request messages.
+
+ Implementations of this method should return a different sequences with each
+ call so that multiple test executions of the test method may be made with
+ different inputs.
+
+ Returns:
+ A sequence of request messages.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def verify(self, requests, responses, test_case):
+ """Verifies that the computed response matches the given requests.
+
+ Args:
+ requests: A sequence of request messages.
+ responses: A sequence of response messages.
+ test_case: A unittest.TestCase object affording useful assertion methods.
+
+ Raises:
+ AssertionError: If the requests and responses do not match, indicating
+ that there was some problem executing the RPC under test.
+ """
+ raise NotImplementedError()
+
+
+class TestService(object):
+ """A specification of implemented methods to use in tests."""
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def unary_unary_scenarios(self):
+ """Affords unary-request-unary-response test methods and their messages.
+
+ Returns:
+ A dict from method group-name pair to implementation/messages pair. The
+ first element of the pair is a UnaryUnaryTestMethodImplementation object
+ and the second element is a sequence of UnaryUnaryTestMethodMessages
+ objects.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def unary_stream_scenarios(self):
+ """Affords unary-request-stream-response test methods and their messages.
+
+ Returns:
+ A dict from method group-name pair to implementation/messages pair. The
+ first element of the pair is a UnaryStreamTestMethodImplementation
+ object and the second element is a sequence of
+ UnaryStreamTestMethodMessages objects.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def stream_unary_scenarios(self):
+ """Affords stream-request-unary-response test methods and their messages.
+
+ Returns:
+ A dict from method group-name pair to implementation/messages pair. The
+ first element of the pair is a StreamUnaryTestMethodImplementation
+ object and the second element is a sequence of
+ StreamUnaryTestMethodMessages objects.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def stream_stream_scenarios(self):
+ """Affords stream-request-stream-response test methods and their messages.
+
+ Returns:
+ A dict from method group-name pair to implementation/messages pair. The
+ first element of the pair is a StreamStreamTestMethodImplementation
+ object and the second element is a sequence of
+ StreamStreamTestMethodMessages objects.
+ """
+ raise NotImplementedError()
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/_stock_service.py b/src/python/grpcio/tests/unit/framework/interfaces/face/_stock_service.py
new file mode 100644
index 0000000000..5299655bb3
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/_stock_service.py
@@ -0,0 +1,396 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Examples of Python implementations of the stock.proto Stock service."""
+
+from grpc.framework.common import cardinality
+from grpc.framework.foundation import abandonment
+from grpc.framework.foundation import stream
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.face import _service
+from tests.unit._junkdrawer import stock_pb2
+
+_STOCK_GROUP_NAME = 'Stock'
+_SYMBOL_FORMAT = 'test symbol:%03d'
+
+# A test-appropriate security-pricing function. :-P
+_price = lambda symbol_name: float(hash(symbol_name) % 4096)
+
+
+def _get_last_trade_price(stock_request, stock_reply_callback, control, active):
+ """A unary-request, unary-response test method."""
+ control.control()
+ if active():
+ stock_reply_callback(
+ stock_pb2.StockReply(
+ symbol=stock_request.symbol, price=_price(stock_request.symbol)))
+ else:
+ raise abandonment.Abandoned()
+
+
+def _get_last_trade_price_multiple(stock_reply_consumer, control, active):
+ """A stream-request, stream-response test method."""
+ def stock_reply_for_stock_request(stock_request):
+ control.control()
+ if active():
+ return stock_pb2.StockReply(
+ symbol=stock_request.symbol, price=_price(stock_request.symbol))
+ else:
+ raise abandonment.Abandoned()
+
+ class StockRequestConsumer(stream.Consumer):
+
+ def consume(self, stock_request):
+ stock_reply_consumer.consume(stock_reply_for_stock_request(stock_request))
+
+ def terminate(self):
+ control.control()
+ stock_reply_consumer.terminate()
+
+ def consume_and_terminate(self, stock_request):
+ stock_reply_consumer.consume_and_terminate(
+ stock_reply_for_stock_request(stock_request))
+
+ return StockRequestConsumer()
+
+
+def _watch_future_trades(stock_request, stock_reply_consumer, control, active):
+ """A unary-request, stream-response test method."""
+ base_price = _price(stock_request.symbol)
+ for index in range(stock_request.num_trades_to_watch):
+ control.control()
+ if active():
+ stock_reply_consumer.consume(
+ stock_pb2.StockReply(
+ symbol=stock_request.symbol, price=base_price + index))
+ else:
+ raise abandonment.Abandoned()
+ stock_reply_consumer.terminate()
+
+
+def _get_highest_trade_price(stock_reply_callback, control, active):
+ """A stream-request, unary-response test method."""
+
+ class StockRequestConsumer(stream.Consumer):
+ """Keeps an ongoing record of the most valuable symbol yet consumed."""
+
+ def __init__(self):
+ self._symbol = None
+ self._price = None
+
+ def consume(self, stock_request):
+ control.control()
+ if active():
+ if self._price is None:
+ self._symbol = stock_request.symbol
+ self._price = _price(stock_request.symbol)
+ else:
+ candidate_price = _price(stock_request.symbol)
+ if self._price < candidate_price:
+ self._symbol = stock_request.symbol
+ self._price = candidate_price
+
+ def terminate(self):
+ control.control()
+ if active():
+ if self._symbol is None:
+ raise ValueError()
+ else:
+ stock_reply_callback(
+ stock_pb2.StockReply(symbol=self._symbol, price=self._price))
+ self._symbol = None
+ self._price = None
+
+ def consume_and_terminate(self, stock_request):
+ control.control()
+ if active():
+ if self._price is None:
+ stock_reply_callback(
+ stock_pb2.StockReply(
+ symbol=stock_request.symbol,
+ price=_price(stock_request.symbol)))
+ else:
+ candidate_price = _price(stock_request.symbol)
+ if self._price < candidate_price:
+ stock_reply_callback(
+ stock_pb2.StockReply(
+ symbol=stock_request.symbol, price=candidate_price))
+ else:
+ stock_reply_callback(
+ stock_pb2.StockReply(
+ symbol=self._symbol, price=self._price))
+
+ self._symbol = None
+ self._price = None
+
+ return StockRequestConsumer()
+
+
+class GetLastTradePrice(_service.UnaryUnaryTestMethodImplementation):
+ """GetLastTradePrice for use in tests."""
+
+ def group(self):
+ return _STOCK_GROUP_NAME
+
+ def name(self):
+ return 'GetLastTradePrice'
+
+ def cardinality(self):
+ return cardinality.Cardinality.UNARY_UNARY
+
+ def request_class(self):
+ return stock_pb2.StockRequest
+
+ def response_class(self):
+ return stock_pb2.StockReply
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, serialized_request):
+ return stock_pb2.StockRequest.FromString(serialized_request)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, serialized_response):
+ return stock_pb2.StockReply.FromString(serialized_response)
+
+ def service(self, request, response_callback, context, control):
+ _get_last_trade_price(
+ request, response_callback, control, context.is_active)
+
+
+class GetLastTradePriceMessages(_service.UnaryUnaryTestMessages):
+
+ def __init__(self):
+ self._index = 0
+
+ def request(self):
+ symbol = _SYMBOL_FORMAT % self._index
+ self._index += 1
+ return stock_pb2.StockRequest(symbol=symbol)
+
+ def verify(self, request, response, test_case):
+ test_case.assertEqual(request.symbol, response.symbol)
+ test_case.assertEqual(_price(request.symbol), response.price)
+
+
+class GetLastTradePriceMultiple(_service.StreamStreamTestMethodImplementation):
+ """GetLastTradePriceMultiple for use in tests."""
+
+ def group(self):
+ return _STOCK_GROUP_NAME
+
+ def name(self):
+ return 'GetLastTradePriceMultiple'
+
+ def cardinality(self):
+ return cardinality.Cardinality.STREAM_STREAM
+
+ def request_class(self):
+ return stock_pb2.StockRequest
+
+ def response_class(self):
+ return stock_pb2.StockReply
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, serialized_request):
+ return stock_pb2.StockRequest.FromString(serialized_request)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, serialized_response):
+ return stock_pb2.StockReply.FromString(serialized_response)
+
+ def service(self, response_consumer, context, control):
+ return _get_last_trade_price_multiple(
+ response_consumer, control, context.is_active)
+
+
+class GetLastTradePriceMultipleMessages(_service.StreamStreamTestMessages):
+ """Pairs of message streams for use with GetLastTradePriceMultiple."""
+
+ def __init__(self):
+ self._index = 0
+
+ def requests(self):
+ base_index = self._index
+ self._index += 1
+ return [
+ stock_pb2.StockRequest(symbol=_SYMBOL_FORMAT % (base_index + index))
+ for index in range(test_constants.STREAM_LENGTH)]
+
+ def verify(self, requests, responses, test_case):
+ test_case.assertEqual(len(requests), len(responses))
+ for stock_request, stock_reply in zip(requests, responses):
+ test_case.assertEqual(stock_request.symbol, stock_reply.symbol)
+ test_case.assertEqual(_price(stock_request.symbol), stock_reply.price)
+
+
+class WatchFutureTrades(_service.UnaryStreamTestMethodImplementation):
+ """WatchFutureTrades for use in tests."""
+
+ def group(self):
+ return _STOCK_GROUP_NAME
+
+ def name(self):
+ return 'WatchFutureTrades'
+
+ def cardinality(self):
+ return cardinality.Cardinality.UNARY_STREAM
+
+ def request_class(self):
+ return stock_pb2.StockRequest
+
+ def response_class(self):
+ return stock_pb2.StockReply
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, serialized_request):
+ return stock_pb2.StockRequest.FromString(serialized_request)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, serialized_response):
+ return stock_pb2.StockReply.FromString(serialized_response)
+
+ def service(self, request, response_consumer, context, control):
+ _watch_future_trades(request, response_consumer, control, context.is_active)
+
+
+class WatchFutureTradesMessages(_service.UnaryStreamTestMessages):
+ """Pairs of a single request message and a sequence of response messages."""
+
+ def __init__(self):
+ self._index = 0
+
+ def request(self):
+ symbol = _SYMBOL_FORMAT % self._index
+ self._index += 1
+ return stock_pb2.StockRequest(
+ symbol=symbol, num_trades_to_watch=test_constants.STREAM_LENGTH)
+
+ def verify(self, request, responses, test_case):
+ test_case.assertEqual(test_constants.STREAM_LENGTH, len(responses))
+ base_price = _price(request.symbol)
+ for index, response in enumerate(responses):
+ test_case.assertEqual(base_price + index, response.price)
+
+
+class GetHighestTradePrice(_service.StreamUnaryTestMethodImplementation):
+ """GetHighestTradePrice for use in tests."""
+
+ def group(self):
+ return _STOCK_GROUP_NAME
+
+ def name(self):
+ return 'GetHighestTradePrice'
+
+ def cardinality(self):
+ return cardinality.Cardinality.STREAM_UNARY
+
+ def request_class(self):
+ return stock_pb2.StockRequest
+
+ def response_class(self):
+ return stock_pb2.StockReply
+
+ def serialize_request(self, request):
+ return request.SerializeToString()
+
+ def deserialize_request(self, serialized_request):
+ return stock_pb2.StockRequest.FromString(serialized_request)
+
+ def serialize_response(self, response):
+ return response.SerializeToString()
+
+ def deserialize_response(self, serialized_response):
+ return stock_pb2.StockReply.FromString(serialized_response)
+
+ def service(self, response_callback, context, control):
+ return _get_highest_trade_price(
+ response_callback, control, context.is_active)
+
+
+class GetHighestTradePriceMessages(_service.StreamUnaryTestMessages):
+
+ def requests(self):
+ return [
+ stock_pb2.StockRequest(symbol=_SYMBOL_FORMAT % index)
+ for index in range(test_constants.STREAM_LENGTH)]
+
+ def verify(self, requests, response, test_case):
+ price = None
+ symbol = None
+ for stock_request in requests:
+ current_symbol = stock_request.symbol
+ current_price = _price(current_symbol)
+ if price is None or price < current_price:
+ price = current_price
+ symbol = current_symbol
+ test_case.assertEqual(price, response.price)
+ test_case.assertEqual(symbol, response.symbol)
+
+
+class StockTestService(_service.TestService):
+ """A corpus of test data with one method of each RPC cardinality."""
+
+ def unary_unary_scenarios(self):
+ return {
+ (_STOCK_GROUP_NAME, 'GetLastTradePrice'): (
+ GetLastTradePrice(), [GetLastTradePriceMessages()]),
+ }
+
+ def unary_stream_scenarios(self):
+ return {
+ (_STOCK_GROUP_NAME, 'WatchFutureTrades'): (
+ WatchFutureTrades(), [WatchFutureTradesMessages()]),
+ }
+
+ def stream_unary_scenarios(self):
+ return {
+ (_STOCK_GROUP_NAME, 'GetHighestTradePrice'): (
+ GetHighestTradePrice(), [GetHighestTradePriceMessages()])
+ }
+
+ def stream_stream_scenarios(self):
+ return {
+ (_STOCK_GROUP_NAME, 'GetLastTradePriceMultiple'): (
+ GetLastTradePriceMultiple(), [GetLastTradePriceMultipleMessages()]),
+ }
+
+
+STOCK_TEST_SERVICE = StockTestService()
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/test_cases.py b/src/python/grpcio/tests/unit/framework/interfaces/face/test_cases.py
new file mode 100644
index 0000000000..462829b660
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/test_cases.py
@@ -0,0 +1,69 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tools for creating tests of implementations of the Face layer."""
+
+# unittest is referenced from specification in this module.
+import unittest # pylint: disable=unused-import
+
+# test_interfaces is referenced from specification in this module.
+from tests.unit.framework.interfaces.face import _blocking_invocation_inline_service
+from tests.unit.framework.interfaces.face import _event_invocation_synchronous_event_service
+from tests.unit.framework.interfaces.face import _future_invocation_asynchronous_event_service
+from tests.unit.framework.interfaces.face import _invocation
+from tests.unit.framework.interfaces.face import test_interfaces # pylint: disable=unused-import
+
+_TEST_CASE_SUPERCLASSES = (
+ _blocking_invocation_inline_service.TestCase,
+ _event_invocation_synchronous_event_service.TestCase,
+ _future_invocation_asynchronous_event_service.TestCase,
+)
+
+
+def test_cases(implementation):
+ """Creates unittest.TestCase classes for a given Face layer implementation.
+
+ Args:
+ implementation: A test_interfaces.Implementation specifying creation and
+ destruction of a given Face layer implementation.
+
+ Returns:
+ A sequence of subclasses of unittest.TestCase defining tests of the
+ specified Face layer implementation.
+ """
+ test_case_classes = []
+ for invoker_constructor in _invocation.invoker_constructors():
+ for super_class in _TEST_CASE_SUPERCLASSES:
+ test_case_classes.append(
+ type(invoker_constructor.name() + super_class.NAME, (super_class,),
+ {'implementation': implementation,
+ 'invoker_constructor': invoker_constructor,
+ '__module__': implementation.__module__,
+ }))
+ return test_case_classes
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/face/test_interfaces.py b/src/python/grpcio/tests/unit/framework/interfaces/face/test_interfaces.py
new file mode 100644
index 0000000000..b2b5c10fa6
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/face/test_interfaces.py
@@ -0,0 +1,229 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Interfaces used in tests of implementations of the Face layer."""
+
+import abc
+
+from grpc.framework.common import cardinality # pylint: disable=unused-import
+from grpc.framework.interfaces.face import face # pylint: disable=unused-import
+
+
+class Method(object):
+ """Specifies a method to be used in tests."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def group(self):
+ """Identify the group of the method.
+
+ Returns:
+ The group of the method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def name(self):
+ """Identify the name of the method.
+
+ Returns:
+ The name of the method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def cardinality(self):
+ """Identify the cardinality of the method.
+
+ Returns:
+ A cardinality.Cardinality value describing the streaming semantics of the
+ method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def request_class(self):
+ """Identify the class used for the method's request objects.
+
+ Returns:
+ The class object of the class to which the method's request objects
+ belong.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def response_class(self):
+ """Identify the class used for the method's response objects.
+
+ Returns:
+ The class object of the class to which the method's response objects
+ belong.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_request(self, request):
+ """Serialize the given request object.
+
+ Args:
+ request: A request object appropriate for this method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_request(self, serialized_request):
+ """Synthesize a request object from a given bytestring.
+
+ Args:
+ serialized_request: A bytestring deserializable into a request object
+ appropriate for this method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def serialize_response(self, response):
+ """Serialize the given response object.
+
+ Args:
+ response: A response object appropriate for this method.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def deserialize_response(self, serialized_response):
+ """Synthesize a response object from a given bytestring.
+
+ Args:
+ serialized_response: A bytestring deserializable into a response object
+ appropriate for this method.
+ """
+ raise NotImplementedError()
+
+
+class Implementation(object):
+ """Specifies an implementation of the Face layer."""
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def instantiate(
+ self, methods, method_implementations,
+ multi_method_implementation):
+ """Instantiates the Face layer implementation to be used in a test.
+
+ Args:
+ methods: A sequence of Method objects describing the methods available to
+ be called during the test.
+ method_implementations: A dictionary from group-name pair to
+ face.MethodImplementation object specifying implementation of a method.
+ multi_method_implementation: A face.MultiMethodImplementation or None.
+
+ Returns:
+ A sequence of length three the first element of which is a
+ face.GenericStub, the second element of which is dictionary from groups
+ to face.DynamicStubs affording invocation of the group's methods, and
+ the third element of which is an arbitrary memo object to be kept and
+ passed to destantiate at the conclusion of the test. The returned stubs
+ must be backed by the provided implementations.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def destantiate(self, memo):
+ """Destroys the Face layer implementation under test.
+
+ Args:
+ memo: The object from the third position of the return value of a call to
+ instantiate.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def invocation_metadata(self):
+ """Provides the metadata to be used when invoking a test RPC.
+
+ Returns:
+ An object to use as the supplied-at-invocation-time metadata in a test
+ RPC.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def initial_metadata(self):
+ """Provides the metadata for use as a test RPC's first servicer metadata.
+
+ Returns:
+ An object to use as the from-the-servicer-before-responses metadata in a
+ test RPC.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def terminal_metadata(self):
+ """Provides the metadata for use as a test RPC's second servicer metadata.
+
+ Returns:
+ An object to use as the from-the-servicer-after-all-responses metadata in
+ a test RPC.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def code(self):
+ """Provides the value for use as a test RPC's code.
+
+ Returns:
+ An object to use as the from-the-servicer code in a test RPC.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def details(self):
+ """Provides the value for use as a test RPC's details.
+
+ Returns:
+ An object to use as the from-the-servicer details in a test RPC.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def metadata_transmitted(self, original_metadata, transmitted_metadata):
+ """Identifies whether or not metadata was properly transmitted.
+
+ Args:
+ original_metadata: A metadata value passed to the Face interface
+ implementation under test.
+ transmitted_metadata: The same metadata value after having been
+ transmitted via an RPC performed by the Face interface implementation
+ under test.
+
+ Returns:
+ Whether or not the metadata was properly transmitted by the Face interface
+ implementation under test.
+ """
+ raise NotImplementedError()
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/links/__init__.py b/src/python/grpcio/tests/unit/framework/interfaces/links/__init__.py
new file mode 100644
index 0000000000..7086519106
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/links/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/links/test_cases.py b/src/python/grpcio/tests/unit/framework/interfaces/links/test_cases.py
new file mode 100644
index 0000000000..dace6c23f3
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/links/test_cases.py
@@ -0,0 +1,326 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Tests of the links interface of RPC Framework."""
+
+# unittest is referenced from specification in this module.
+import abc
+import unittest # pylint: disable=unused-import
+
+from grpc.framework.interfaces.links import links
+from tests.unit.framework.common import test_constants
+from tests.unit.framework.interfaces.links import test_utilities
+
+
+def at_least_n_payloads_received_predicate(n):
+ def predicate(ticket_sequence):
+ payload_count = 0
+ for ticket in ticket_sequence:
+ if ticket.payload is not None:
+ payload_count += 1
+ if n <= payload_count:
+ return True
+ else:
+ return False
+ return predicate
+
+
+def terminated(ticket_sequence):
+ return ticket_sequence and ticket_sequence[-1].termination is not None
+
+_TRANSMISSION_GROUP = 'test.Group'
+_TRANSMISSION_METHOD = 'TestMethod'
+
+
+class TransmissionTest(object):
+ """Tests ticket transmission between two connected links.
+
+ This class must be mixed into a unittest.TestCase that implements the abstract
+ methods it provides.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ # This is a unittest.TestCase mix-in.
+ # pylint: disable=invalid-name
+
+ @abc.abstractmethod
+ def create_transmitting_links(self):
+ """Creates two connected links for use in this test.
+
+ Returns:
+ Two links.Links, the first of which will be used on the invocation side
+ of RPCs and the second of which will be used on the service side of
+ RPCs.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def destroy_transmitting_links(self, invocation_side_link, service_side_link):
+ """Destroys the two connected links created for this test.
+
+
+ Args:
+ invocation_side_link: The link used on the invocation side of RPCs in
+ this test.
+ service_side_link: The link used on the service side of RPCs in this
+ test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def create_invocation_initial_metadata(self):
+ """Creates a value for use as invocation-side initial metadata.
+
+ Returns:
+ A metadata value appropriate for use as invocation-side initial metadata
+ or None if invocation-side initial metadata transmission is not
+ supported by the links under test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def create_invocation_terminal_metadata(self):
+ """Creates a value for use as invocation-side terminal metadata.
+
+ Returns:
+ A metadata value appropriate for use as invocation-side terminal
+ metadata or None if invocation-side terminal metadata transmission is
+ not supported by the links under test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def create_service_initial_metadata(self):
+ """Creates a value for use as service-side initial metadata.
+
+ Returns:
+ A metadata value appropriate for use as service-side initial metadata or
+ None if service-side initial metadata transmission is not supported by
+ the links under test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def create_service_terminal_metadata(self):
+ """Creates a value for use as service-side terminal metadata.
+
+ Returns:
+ A metadata value appropriate for use as service-side terminal metadata or
+ None if service-side terminal metadata transmission is not supported by
+ the links under test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def create_invocation_completion(self):
+ """Creates values for use as invocation-side code and message.
+
+ Returns:
+ An invocation-side code value and an invocation-side message value.
+ Either or both may be None if invocation-side code and/or
+ invocation-side message transmission is not supported by the links
+ under test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def create_service_completion(self):
+ """Creates values for use as service-side code and message.
+
+ Returns:
+ A service-side code value and a service-side message value. Either or
+ both may be None if service-side code and/or service-side message
+ transmission is not supported by the links under test.
+ """
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def assertMetadataTransmitted(self, original_metadata, transmitted_metadata):
+ """Asserts that transmitted_metadata contains original_metadata.
+
+ Args:
+ original_metadata: A metadata object used in this test.
+ transmitted_metadata: A metadata object obtained after transmission
+ through the system under test.
+
+ Raises:
+ AssertionError: if the transmitted_metadata object does not contain
+ original_metadata.
+ """
+ raise NotImplementedError()
+
+ def group_and_method(self):
+ """Returns the group and method used in this test case.
+
+ Returns:
+ A pair of the group and method used in this test case.
+ """
+ return _TRANSMISSION_GROUP, _TRANSMISSION_METHOD
+
+ def serialize_request(self, request):
+ """Serializes a request value used in this test case.
+
+ Args:
+ request: A request value created by this test case.
+
+ Returns:
+ A bytestring that is the serialization of the given request.
+ """
+ return request
+
+ def deserialize_request(self, serialized_request):
+ """Deserializes a request value used in this test case.
+
+ Args:
+ serialized_request: A bytestring that is the serialization of some request
+ used in this test case.
+
+ Returns:
+ The request value encoded by the given bytestring.
+ """
+ return serialized_request
+
+ def serialize_response(self, response):
+ """Serializes a response value used in this test case.
+
+ Args:
+ response: A response value created by this test case.
+
+ Returns:
+ A bytestring that is the serialization of the given response.
+ """
+ return response
+
+ def deserialize_response(self, serialized_response):
+ """Deserializes a response value used in this test case.
+
+ Args:
+ serialized_response: A bytestring that is the serialization of some
+ response used in this test case.
+
+ Returns:
+ The response value encoded by the given bytestring.
+ """
+ return serialized_response
+
+ def _assert_is_valid_metadata_payload_sequence(
+ self, ticket_sequence, payloads, initial_metadata, terminal_metadata):
+ initial_metadata_seen = False
+ seen_payloads = []
+ terminal_metadata_seen = False
+
+ for ticket in ticket_sequence:
+ if ticket.initial_metadata is not None:
+ self.assertFalse(initial_metadata_seen)
+ self.assertFalse(seen_payloads)
+ self.assertFalse(terminal_metadata_seen)
+ self.assertMetadataTransmitted(initial_metadata, ticket.initial_metadata)
+ initial_metadata_seen = True
+
+ if ticket.payload is not None:
+ self.assertFalse(terminal_metadata_seen)
+ seen_payloads.append(ticket.payload)
+
+ if ticket.terminal_metadata is not None:
+ self.assertFalse(terminal_metadata_seen)
+ self.assertMetadataTransmitted(terminal_metadata, ticket.terminal_metadata)
+ terminal_metadata_seen = True
+ self.assertSequenceEqual(payloads, seen_payloads)
+
+ def _assert_is_valid_invocation_sequence(
+ self, ticket_sequence, group, method, payloads, initial_metadata,
+ terminal_metadata, termination):
+ self.assertLess(0, len(ticket_sequence))
+ self.assertEqual(group, ticket_sequence[0].group)
+ self.assertEqual(method, ticket_sequence[0].method)
+ self._assert_is_valid_metadata_payload_sequence(
+ ticket_sequence, payloads, initial_metadata, terminal_metadata)
+ self.assertIs(termination, ticket_sequence[-1].termination)
+
+ def _assert_is_valid_service_sequence(
+ self, ticket_sequence, payloads, initial_metadata, terminal_metadata,
+ code, message, termination):
+ self.assertLess(0, len(ticket_sequence))
+ self._assert_is_valid_metadata_payload_sequence(
+ ticket_sequence, payloads, initial_metadata, terminal_metadata)
+ self.assertEqual(code, ticket_sequence[-1].code)
+ self.assertEqual(message, ticket_sequence[-1].message)
+ self.assertIs(termination, ticket_sequence[-1].termination)
+
+ def setUp(self):
+ self._invocation_link, self._service_link = self.create_transmitting_links()
+ self._invocation_mate = test_utilities.RecordingLink()
+ self._service_mate = test_utilities.RecordingLink()
+ self._invocation_link.join_link(self._invocation_mate)
+ self._service_link.join_link(self._service_mate)
+
+ def tearDown(self):
+ self.destroy_transmitting_links(self._invocation_link, self._service_link)
+
+ def testSimplestRoundTrip(self):
+ """Tests transmission of one ticket in each direction."""
+ invocation_operation_id = object()
+ invocation_payload = b'\x07' * 1023
+ timeout = test_constants.LONG_TIMEOUT
+ invocation_initial_metadata = self.create_invocation_initial_metadata()
+ invocation_terminal_metadata = self.create_invocation_terminal_metadata()
+ invocation_code, invocation_message = self.create_invocation_completion()
+ service_payload = b'\x08' * 1025
+ service_initial_metadata = self.create_service_initial_metadata()
+ service_terminal_metadata = self.create_service_terminal_metadata()
+ service_code, service_message = self.create_service_completion()
+
+ original_invocation_ticket = links.Ticket(
+ invocation_operation_id, 0, _TRANSMISSION_GROUP, _TRANSMISSION_METHOD,
+ links.Ticket.Subscription.FULL, timeout, 0, invocation_initial_metadata,
+ invocation_payload, invocation_terminal_metadata, invocation_code,
+ invocation_message, links.Ticket.Termination.COMPLETION, None)
+ self._invocation_link.accept_ticket(original_invocation_ticket)
+
+ self._service_mate.block_until_tickets_satisfy(
+ at_least_n_payloads_received_predicate(1))
+ service_operation_id = self._service_mate.tickets()[0].operation_id
+
+ self._service_mate.block_until_tickets_satisfy(terminated)
+ self._assert_is_valid_invocation_sequence(
+ self._service_mate.tickets(), _TRANSMISSION_GROUP, _TRANSMISSION_METHOD,
+ (invocation_payload,), invocation_initial_metadata,
+ invocation_terminal_metadata, links.Ticket.Termination.COMPLETION)
+
+ original_service_ticket = links.Ticket(
+ service_operation_id, 0, None, None, links.Ticket.Subscription.FULL,
+ timeout, 0, service_initial_metadata, service_payload,
+ service_terminal_metadata, service_code, service_message,
+ links.Ticket.Termination.COMPLETION, None)
+ self._service_link.accept_ticket(original_service_ticket)
+ self._invocation_mate.block_until_tickets_satisfy(terminated)
+ self._assert_is_valid_service_sequence(
+ self._invocation_mate.tickets(), (service_payload,),
+ service_initial_metadata, service_terminal_metadata, service_code,
+ service_message, links.Ticket.Termination.COMPLETION)
diff --git a/src/python/grpcio/tests/unit/framework/interfaces/links/test_utilities.py b/src/python/grpcio/tests/unit/framework/interfaces/links/test_utilities.py
new file mode 100644
index 0000000000..39c7f2fc63
--- /dev/null
+++ b/src/python/grpcio/tests/unit/framework/interfaces/links/test_utilities.py
@@ -0,0 +1,167 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""State and behavior appropriate for use in tests."""
+
+import logging
+import threading
+import time
+
+from grpc.framework.interfaces.links import links
+from grpc.framework.interfaces.links import utilities
+
+# A more-or-less arbitrary limit on the length of raw data values to be logged.
+_UNCOMFORTABLY_LONG = 48
+
+
+def _safe_for_log_ticket(ticket):
+ """Creates a safe-for-printing-to-the-log ticket for a given ticket.
+
+ Args:
+ ticket: Any links.Ticket.
+
+ Returns:
+ A links.Ticket that is as much as can be equal to the given ticket but
+ possibly features values like the string "<payload of length 972321>" in
+ place of the actual values of the given ticket.
+ """
+ if isinstance(ticket.payload, (basestring,)):
+ payload_length = len(ticket.payload)
+ else:
+ payload_length = -1
+ if payload_length < _UNCOMFORTABLY_LONG:
+ return ticket
+ else:
+ return links.Ticket(
+ ticket.operation_id, ticket.sequence_number,
+ ticket.group, ticket.method, ticket.subscription, ticket.timeout,
+ ticket.allowance, ticket.initial_metadata,
+ '<payload of length {}>'.format(payload_length),
+ ticket.terminal_metadata, ticket.code, ticket.message,
+ ticket.termination, None)
+
+
+class RecordingLink(links.Link):
+ """A Link that records every ticket passed to it."""
+
+ def __init__(self):
+ self._condition = threading.Condition()
+ self._tickets = []
+
+ def accept_ticket(self, ticket):
+ with self._condition:
+ self._tickets.append(ticket)
+ self._condition.notify_all()
+
+ def join_link(self, link):
+ pass
+
+ def block_until_tickets_satisfy(self, predicate):
+ """Blocks until the received tickets satisfy the given predicate.
+
+ Args:
+ predicate: A callable that takes a sequence of tickets and returns a
+ boolean value.
+ """
+ with self._condition:
+ while not predicate(self._tickets):
+ self._condition.wait()
+
+ def tickets(self):
+ """Returns a copy of the list of all tickets received by this Link."""
+ with self._condition:
+ return tuple(self._tickets)
+
+
+class _Pipe(object):
+ """A conduit that logs all tickets passed through it."""
+
+ def __init__(self, name):
+ self._lock = threading.Lock()
+ self._name = name
+ self._left_mate = utilities.NULL_LINK
+ self._right_mate = utilities.NULL_LINK
+
+ def accept_left_to_right_ticket(self, ticket):
+ with self._lock:
+ logging.warning(
+ '%s: moving left to right through %s: %s', time.time(), self._name,
+ _safe_for_log_ticket(ticket))
+ try:
+ self._right_mate.accept_ticket(ticket)
+ except Exception as e: # pylint: disable=broad-except
+ logging.exception(e)
+
+ def accept_right_to_left_ticket(self, ticket):
+ with self._lock:
+ logging.warning(
+ '%s: moving right to left through %s: %s', time.time(), self._name,
+ _safe_for_log_ticket(ticket))
+ try:
+ self._left_mate.accept_ticket(ticket)
+ except Exception as e: # pylint: disable=broad-except
+ logging.exception(e)
+
+ def join_left_mate(self, left_mate):
+ with self._lock:
+ self._left_mate = utilities.NULL_LINK if left_mate is None else left_mate
+
+ def join_right_mate(self, right_mate):
+ with self._lock:
+ self._right_mate = (
+ utilities.NULL_LINK if right_mate is None else right_mate)
+
+
+class _Facade(links.Link):
+
+ def __init__(self, accept, join):
+ self._accept = accept
+ self._join = join
+
+ def accept_ticket(self, ticket):
+ self._accept(ticket)
+
+ def join_link(self, link):
+ self._join(link)
+
+
+def logging_links(name):
+ """Creates a conduit that logs all tickets passed through it.
+
+ Args:
+ name: A name to use for the conduit to identify itself in logging output.
+
+ Returns:
+ Two links.Links, the first of which is the "left" side of the conduit
+ and the second of which is the "right" side of the conduit.
+ """
+ pipe = _Pipe(name)
+ left_facade = _Facade(pipe.accept_left_to_right_ticket, pipe.join_left_mate)
+ right_facade = _Facade(pipe.accept_right_to_left_ticket, pipe.join_right_mate)
+ return left_facade, right_facade
diff --git a/src/python/grpcio/tests/unit/resources.py b/src/python/grpcio/tests/unit/resources.py
new file mode 100644
index 0000000000..023cdb155f
--- /dev/null
+++ b/src/python/grpcio/tests/unit/resources.py
@@ -0,0 +1,52 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Constants and functions for data used in interoperability testing."""
+
+import os
+
+import pkg_resources
+
+_ROOT_CERTIFICATES_RESOURCE_PATH = 'credentials/ca.pem'
+_PRIVATE_KEY_RESOURCE_PATH = 'credentials/server1.key'
+_CERTIFICATE_CHAIN_RESOURCE_PATH = 'credentials/server1.pem'
+
+
+def test_root_certificates():
+ return pkg_resources.resource_string(
+ __name__, _ROOT_CERTIFICATES_RESOURCE_PATH)
+
+
+def private_key():
+ return pkg_resources.resource_string(__name__, _PRIVATE_KEY_RESOURCE_PATH)
+
+
+def certificate_chain():
+ return pkg_resources.resource_string(
+ __name__, _CERTIFICATE_CHAIN_RESOURCE_PATH)
diff --git a/src/python/grpcio/tests/unit/test_common.py b/src/python/grpcio/tests/unit/test_common.py
new file mode 100644
index 0000000000..29431bfb9d
--- /dev/null
+++ b/src/python/grpcio/tests/unit/test_common.py
@@ -0,0 +1,80 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Common code used throughout tests of gRPC."""
+
+import collections
+
+INVOCATION_INITIAL_METADATA = ((b'0', b'abc'), (b'1', b'def'), (b'2', b'ghi'),)
+SERVICE_INITIAL_METADATA = ((b'3', b'jkl'), (b'4', b'mno'), (b'5', b'pqr'),)
+SERVICE_TERMINAL_METADATA = ((b'6', b'stu'), (b'7', b'vwx'), (b'8', b'yza'),)
+DETAILS = b'test details'
+
+
+def metadata_transmitted(original_metadata, transmitted_metadata):
+ """Judges whether or not metadata was acceptably transmitted.
+
+ gRPC is allowed to insert key-value pairs into the metadata values given by
+ applications and to reorder key-value pairs with different keys but it is not
+ allowed to alter existing key-value pairs or to reorder key-value pairs with
+ the same key.
+
+ Args:
+ original_metadata: A metadata value used in a test of gRPC. An iterable over
+ iterables of length 2.
+ transmitted_metadata: A metadata value corresponding to original_metadata
+ after having been transmitted via gRPC. An iterable over iterables of
+ length 2.
+
+ Returns:
+ A boolean indicating whether transmitted_metadata accurately reflects
+ original_metadata after having been transmitted via gRPC.
+ """
+ original = collections.defaultdict(list)
+ for key_value_pair in original_metadata:
+ key, value = tuple(key_value_pair)
+ original[key].append(value)
+ transmitted = collections.defaultdict(list)
+ for key_value_pair in transmitted_metadata:
+ key, value = tuple(key_value_pair)
+ transmitted[key].append(value)
+
+ for key, values in original.iteritems():
+ transmitted_values = transmitted[key]
+ transmitted_iterator = iter(transmitted_values)
+ try:
+ for value in values:
+ while True:
+ transmitted_value = next(transmitted_iterator)
+ if value == transmitted_value:
+ break
+ except StopIteration:
+ return False
+ else:
+ return True