Organize Python tests to use grpcio-tools directly

Moves all tests into a separate package. This does not change existing
supported means of running tests (e.g. through run_tests.py).
diff --git a/src/python/grpcio_tests/commands.py b/src/python/grpcio_tests/commands.py
new file mode 100644
index 0000000..171829b
--- /dev/null
+++ b/src/python/grpcio_tests/commands.py
@@ -0,0 +1,217 @@
+# 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.
+
+"""Provides distutils command classes for the gRPC Python setup process."""
+
+import distutils
+import glob
+import os
+import os.path
+import platform
+import re
+import shutil
+import subprocess
+import sys
+import traceback
+
+import setuptools
+from setuptools.command import build_ext
+from setuptools.command import build_py
+from setuptools.command import easy_install
+from setuptools.command import install
+from setuptools.command import test
+
+PYTHON_STEM = os.path.dirname(os.path.abspath(__file__))
+GRPC_STEM = os.path.abspath(PYTHON_STEM + '../../../../')
+GRPC_PROTO_STEM = os.path.join(GRPC_STEM, 'src', 'proto')
+PROTO_STEM = os.path.join(PYTHON_STEM, 'src', 'proto')
+PYTHON_PROTO_TOP_LEVEL = os.path.join(PYTHON_STEM, 'src')
+
+
+class CommandError(object):
+  pass
+
+
+class GatherProto(setuptools.Command):
+
+  description = 'gather proto dependencies'
+  user_options = []
+
+  def initialize_options(self):
+    pass
+
+  def finalize_options(self):
+    pass
+
+  def run(self):
+    # TODO(atash) ensure that we're running from the repository directory when
+    # this command is used
+    try:
+      shutil.rmtree(PROTO_STEM)
+    except Exception as error:
+      # We don't care if this command fails
+      pass
+    shutil.copytree(GRPC_PROTO_STEM, PROTO_STEM)
+    for root, _, _ in os.walk(PYTHON_PROTO_TOP_LEVEL):
+      path = os.path.join(root, '__init__.py')
+      open(path, 'a').close()
+
+
+class BuildProtoModules(setuptools.Command):
+  """Command to generate project *_pb2.py modules from proto files."""
+
+  description = 'build protobuf modules'
+  user_options = [
+    ('include=', None, 'path patterns to include in protobuf generation'),
+    ('exclude=', None, 'path patterns to exclude from protobuf generation')
+  ]
+
+  def initialize_options(self):
+    self.exclude = None
+    self.include = r'.*\.proto$'
+
+  def finalize_options(self):
+    pass
+
+  def run(self):
+    import grpc.tools.protoc as protoc
+
+    include_regex = re.compile(self.include)
+    exclude_regex = re.compile(self.exclude) if self.exclude else None
+    paths = []
+    for walk_root, directories, filenames in os.walk(PROTO_STEM):
+      for filename in filenames:
+        path = os.path.join(walk_root, filename)
+        if include_regex.match(path) and not (
+            exclude_regex and exclude_regex.match(path)):
+          paths.append(path)
+
+    # TODO(kpayson): It would be nice to do this in a batch command,
+    # but we currently have name conflicts in src/proto
+    for path in paths:
+      command = [
+          'grpc.tools.protoc',
+          '-I {}'.format(PROTO_STEM),
+          '--python_out={}'.format(PROTO_STEM),
+          '--grpc_python_out={}'.format(PROTO_STEM),
+      ] + [path]
+      if protoc.main(command) != 0:
+        sys.stderr.write(
+            'warning: Command:\n{}\nFailed'.format(
+                command))
+
+    # Generated proto directories dont include __init__.py, but
+    # these are needed for python package resolution
+    for walk_root, _, _ in os.walk(PROTO_STEM):
+      path = os.path.join(walk_root, '__init__.py')
+      open(path, 'a').close()
+
+
+class BuildPy(build_py.build_py):
+  """Custom project build command."""
+
+  def run(self):
+    try:
+      self.run_command('build_proto_modules')
+    except CommandError as error:
+      sys.stderr.write('warning: %s\n' % error.message)
+    build_py.build_py.run(self)
+
+
+class TestLite(setuptools.Command):
+  """Command to run tests without fetching or building anything."""
+
+  description = 'run tests without fetching or building anything.'
+  user_options = []
+
+  def initialize_options(self):
+    pass
+
+  def finalize_options(self):
+    # distutils requires this override.
+    pass
+
+  def run(self):
+    self._add_eggs_to_path()
+
+    import tests
+    loader = tests.Loader()
+    loader.loadTestsFromNames(['tests'])
+    runner = tests.Runner()
+    result = runner.run(loader.suite)
+    if not result.wasSuccessful():
+      sys.exit('Test failure')
+
+  def _add_eggs_to_path(self):
+    """Fetch install and test requirements"""
+    self.distribution.fetch_build_eggs(self.distribution.install_requires)
+    self.distribution.fetch_build_eggs(self.distribution.tests_require)
+
+
+class RunInterop(test.test):
+
+  description = 'run interop test client/server'
+  user_options = [
+    ('args=', 'a', 'pass-thru arguments for the client/server'),
+    ('client', 'c', 'flag indicating to run the client'),
+    ('server', 's', 'flag indicating to run the server')
+  ]
+
+  def initialize_options(self):
+    self.args = ''
+    self.client = False
+    self.server = False
+
+  def finalize_options(self):
+    if self.client and self.server:
+      raise DistutilsOptionError('you may only specify one of client or server')
+
+  def run(self):
+    if self.distribution.install_requires:
+      self.distribution.fetch_build_eggs(self.distribution.install_requires)
+    if self.distribution.tests_require:
+      self.distribution.fetch_build_eggs(self.distribution.tests_require)
+    if self.client:
+      self.run_client()
+    elif self.server:
+      self.run_server()
+
+  def run_server(self):
+    # We import here to ensure that our setuptools parent has had a chance to
+    # edit the Python system path.
+    from tests.interop import server
+    sys.argv[1:] = self.args.split()
+    server.serve()
+
+  def run_client(self):
+    # We import here to ensure that our setuptools parent has had a chance to
+    # edit the Python system path.
+    from tests.interop import client
+    sys.argv[1:] = self.args.split()
+    client.test_interoperability()