You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@thrift.apache.org by ns...@apache.org on 2015/11/06 13:26:55 UTC

[3/4] thrift git commit: THRIFT-1857 Python 3 Support Client: Python Patch: Nobuaki Sukegawa

THRIFT-1857 Python 3 Support
Client: Python
Patch: Nobuaki Sukegawa

Add py3 cross test


Project: http://git-wip-us.apache.org/repos/asf/thrift/repo
Commit: http://git-wip-us.apache.org/repos/asf/thrift/commit/a185d7e7
Tree: http://git-wip-us.apache.org/repos/asf/thrift/tree/a185d7e7
Diff: http://git-wip-us.apache.org/repos/asf/thrift/diff/a185d7e7

Branch: refs/heads/master
Commit: a185d7e78589a42e076379ae7165857e5e828e5c
Parents: 760511f
Author: Nobuaki Sukegawa <ns...@apache.org>
Authored: Fri Nov 6 21:24:24 2015 +0900
Committer: Nobuaki Sukegawa <ns...@apache.org>
Committed: Fri Nov 6 21:24:24 2015 +0900

----------------------------------------------------------------------
 Makefile.am                         |   2 +-
 build/docker/centos/Dockerfile      |   2 +-
 build/docker/ubuntu/Dockerfile      |   2 +-
 build/travis/installDependencies.sh |   2 +-
 configure.ac                        |  12 ++
 contrib/Vagrantfile                 |   2 +-
 lib/py/Makefile.am                  |  11 +-
 test/known_failures_Linux.json      |  14 ++
 test/py/TestClient.py               | 129 ++++++++--------
 test/py/TestServer.py               | 251 ++++++++++++++++---------------
 test/tests.json                     |  39 +++++
 11 files changed, 280 insertions(+), 186 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/Makefile.am
----------------------------------------------------------------------
diff --git a/Makefile.am b/Makefile.am
index 73bc48f..812a40a 100755
--- a/Makefile.am
+++ b/Makefile.am
@@ -49,7 +49,7 @@ empty :=
 space := $(empty) $(empty)
 comma := ,
 
-CROSS_LANGS = @MAYBE_CPP@ @MAYBE_C_GLIB@ @MAYBE_JAVA@ @MAYBE_CSHARP@ @MAYBE_PYTHON@ @MAYBE_RUBY@ @MAYBE_HASKELL@ @MAYBE_PERL@ @MAYBE_PHP@ @MAYBE_GO@ @MAYBE_NODEJS@ @MAYBE_DART@ @MAYBE_ERLANG@
+CROSS_LANGS = @MAYBE_CPP@ @MAYBE_C_GLIB@ @MAYBE_JAVA@ @MAYBE_CSHARP@ @MAYBE_PYTHON@ @MAYBE_PY3@ @MAYBE_RUBY@ @MAYBE_HASKELL@ @MAYBE_PERL@ @MAYBE_PHP@ @MAYBE_GO@ @MAYBE_NODEJS@ @MAYBE_DART@ @MAYBE_ERLANG@
 CROSS_LANGS_COMMA_SEPARATED = $(subst $(space),$(comma),$(CROSS_LANGS))
 
 cross: precross

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/build/docker/centos/Dockerfile
----------------------------------------------------------------------
diff --git a/build/docker/centos/Dockerfile b/build/docker/centos/Dockerfile
index 8d5596a..2ba73c3 100644
--- a/build/docker/centos/Dockerfile
+++ b/build/docker/centos/Dockerfile
@@ -50,7 +50,7 @@ RUN yum install -y libboost-dev libevent-devel
 RUN yum install -y ant junit ant-nodeps ant-junit java-1.7.0-openjdk-devel
 
 # Python Dependencies
-RUN yum install -y python-devel python-setuptools python-twisted
+RUN yum install -y python-devel python-setuptools python-twisted python-six
 
 # Ruby Dependencies
 RUN yum install -y ruby ruby-devel rubygems && \

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/build/docker/ubuntu/Dockerfile
----------------------------------------------------------------------
diff --git a/build/docker/ubuntu/Dockerfile b/build/docker/ubuntu/Dockerfile
index 0b02a70..d8bac0c 100644
--- a/build/docker/ubuntu/Dockerfile
+++ b/build/docker/ubuntu/Dockerfile
@@ -37,7 +37,7 @@ RUN apt-get install -y ant openjdk-7-jdk maven && \
 
 # Python dependencies
 RUN apt-get install -y python-all python-all-dev python-all-dbg python-setuptools python-support \
-    python-twisted python-zope.interface python-six
+    python-twisted python-zope.interface python-six python3-six
 
 # Ruby dependencies
 RUN apt-get install -y ruby ruby-dev && \

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/build/travis/installDependencies.sh
----------------------------------------------------------------------
diff --git a/build/travis/installDependencies.sh b/build/travis/installDependencies.sh
index df12640..4945fd4 100755
--- a/build/travis/installDependencies.sh
+++ b/build/travis/installDependencies.sh
@@ -27,7 +27,7 @@ sudo apt-get install -qq ant openjdk-7-jdk
 sudo update-java-alternatives -s java-1.7.0-openjdk-amd64
 
 # Python dependencies
-sudo apt-get install -qq python-all python-all-dev python-all-dbg python-setuptools python-support python-twisted python-six
+sudo apt-get install -qq python-all python-all-dev python-all-dbg python-setuptools python-support python-twisted python-six python3-six
 
 # Ruby dependencies
 sudo apt-get install -qq ruby ruby-dev

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/configure.ac
----------------------------------------------------------------------
diff --git a/configure.ac b/configure.ac
index 3d1b15f..777300a 100755
--- a/configure.ac
+++ b/configure.ac
@@ -285,6 +285,16 @@ if test "$with_python" = "yes";  then
 fi
 AM_CONDITIONAL(WITH_PYTHON, [test "$have_python" = "yes"])
 
+# Find "python3" executable.
+# It's distro specific and far from ideal but needed to cross test py2-3 at once.
+if test "x$have_python" = "xyes"; then
+  AC_PATH_PROG([PYTHON3], [python3])
+  if test "x$PYTHON3" != "x" && test "x$PYTHON3" != "x:" ; then
+    have_py3="yes"
+  fi
+fi
+AM_CONDITIONAL(WITH_PY3, [test "$have_py3" = "yes"])
+
 AX_THRIFT_LIB(perl, [Perl], yes)
 if test "$with_perl" = "yes"; then
   AC_PATH_PROG([PERL], [perl])
@@ -782,6 +792,8 @@ if test "$have_csharp" = "yes" ; then MAYBE_CSHARP="csharp" ; else MAYBE_CSHARP=
 AC_SUBST([MAYBE_CSHARP])
 if test "$have_python" = "yes" ; then MAYBE_PYTHON="py" ; else MAYBE_PYTHON="" ; fi
 AC_SUBST([MAYBE_PYTHON])
+if test "$have_py3" = "yes" ; then MAYBE_PY3="py3" ; else MAYBE_PY3="" ; fi
+AC_SUBST([MAYBE_PY3])
 if test "$have_ruby" = "yes" ; then MAYBE_RUBY="rb" ; else MAYBE_RUBY="" ; fi
 AC_SUBST([MAYBE_RUBY])
 if test "$have_haskell" = "yes" ; then MAYBE_HASKELL="hs" ; else MAYBE_HASKELL="" ; fi

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/contrib/Vagrantfile
----------------------------------------------------------------------
diff --git a/contrib/Vagrantfile b/contrib/Vagrantfile
index 2091110..3bcc46a 100644
--- a/contrib/Vagrantfile
+++ b/contrib/Vagrantfile
@@ -46,7 +46,7 @@ sudo apt-get install -qq libboost-dev libboost-test-dev libboost-program-options
 sudo apt-get install -qq ant openjdk-7-jdk maven
 
 # Python dependencies
-sudo apt-get install -qq python-all python-all-dev python-all-dbg python-setuptools python-support python-six
+sudo apt-get install -qq python-all python-all-dev python-all-dbg python-setuptools python-support python-six python3-six
 
 # Ruby dependencies
 sudo apt-get install -qq ruby ruby-dev

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/lib/py/Makefile.am
----------------------------------------------------------------------
diff --git a/lib/py/Makefile.am b/lib/py/Makefile.am
index 2cdbb24..b88b6be 100755
--- a/lib/py/Makefile.am
+++ b/lib/py/Makefile.am
@@ -19,7 +19,16 @@
 AUTOMAKE_OPTIONS = serial-tests
 DESTDIR ?= /
 
-all-local:
+if WITH_PY3
+py3-build:
+	$(PYTHON3) setup.py build
+else
+py3-build:
+endif
+
+testing:
+
+all-local: py3-build testing
 	$(PYTHON) setup.py build
 
 # We're ignoring prefix here because site-packages seems to be

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/test/known_failures_Linux.json
----------------------------------------------------------------------
diff --git a/test/known_failures_Linux.json b/test/known_failures_Linux.json
index 4c78c52..3625fa9 100644
--- a/test/known_failures_Linux.json
+++ b/test/known_failures_Linux.json
@@ -88,6 +88,8 @@
   "hs-java_json_framed-ip",
   "hs-nodejs_json_buffered-ip",
   "hs-nodejs_json_framed-ip",
+  "hs-py3_json_buffered-ip",
+  "hs-py3_json_framed-ip",
   "hs-py_json_buffered-ip",
   "hs-py_json_framed-ip",
   "hs-rb_json_buffered-ip",
@@ -120,6 +122,14 @@
   "nodejs-java_compact_framed-ip",
   "nodejs-java_compact_framed-ip-ssl",
   "nodejs-java_json_buffered-ip-ssl",
+  "nodejs-py3_compact_buffered-ip",
+  "nodejs-py3_compact_buffered-ip-ssl",
+  "nodejs-py3_compact_framed-ip",
+  "nodejs-py3_compact_framed-ip-ssl",
+  "nodejs-py3_json_buffered-ip",
+  "nodejs-py3_json_buffered-ip-ssl",
+  "nodejs-py3_json_framed-ip",
+  "nodejs-py3_json_framed-ip-ssl",
   "nodejs-py_compact_buffered-ip",
   "nodejs-py_compact_buffered-ip-ssl",
   "nodejs-py_compact_framed-ip",
@@ -139,6 +149,10 @@
   "py-nodejs_json_buffered-ip-ssl",
   "py-nodejs_json_framed-ip",
   "py-nodejs_json_framed-ip-ssl",
+  "py3-hs_json_buffered-ip",
+  "py3-hs_json_framed-ip",
+  "py3-perl_binary_buffered-ip-ssl",
+  "py3-perl_binary_framed-ip-ssl",
   "rb-hs_json_buffered-ip",
   "rb-hs_json_framed-ip",
   "rb-nodejs_json_buffered-ip",

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/test/py/TestClient.py
----------------------------------------------------------------------
diff --git a/test/py/TestClient.py b/test/py/TestClient.py
index 4689d63..5b858ef 100755
--- a/test/py/TestClient.py
+++ b/test/py/TestClient.py
@@ -26,47 +26,9 @@ import time
 import unittest
 from optparse import OptionParser
 
-parser = OptionParser()
-parser.add_option('--genpydir', type='string', dest='genpydir',
-                  default='gen-py',
-                  help='include this local directory in sys.path for locating generated code')
-parser.add_option("--port", type="int", dest="port",
-    help="connect to server at port")
-parser.add_option("--host", type="string", dest="host",
-    help="connect to server")
-parser.add_option("--zlib", action="store_true", dest="zlib",
-    help="use zlib wrapper for compressed transport")
-parser.add_option("--ssl", action="store_true", dest="ssl",
-    help="use SSL for encrypted transport")
-parser.add_option("--http", dest="http_path",
-    help="Use the HTTP transport with the specified path")
-parser.add_option('-v', '--verbose', action="store_const",
-    dest="verbose", const=2,
-    help="verbose output")
-parser.add_option('-q', '--quiet', action="store_const",
-    dest="verbose", const=0,
-    help="minimal output")
-parser.add_option('--protocol',  dest="proto", type="string",
-    help="protocol to use, one of: accel, binary, compact, json")
-parser.add_option('--transport',  dest="trans", type="string",
-    help="transport to use, one of: buffered, framed")
-parser.set_defaults(framed=False, http_path=None, verbose=1, host='localhost', port=9090, proto='binary')
-options, args = parser.parse_args()
-
-script_dir = os.path.abspath(os.path.dirname(__file__))
-lib_dir = os.path.join(os.path.dirname(os.path.dirname(script_dir)), 'lib', 'py', 'build', 'lib*')
-sys.path.insert(0, os.path.join(script_dir, options.genpydir))
-sys.path.insert(0, glob.glob(lib_dir)[0])
-
-from ThriftTest import ThriftTest
-from ThriftTest.ttypes import *
-from thrift.transport import TTransport
-from thrift.transport import TSocket
-from thrift.transport import THttpClient
-from thrift.transport import TZlibTransport
-from thrift.protocol import TBinaryProtocol
-from thrift.protocol import TCompactProtocol
-from thrift.protocol import TJSONProtocol
+SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
+ROOT_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR))
+DEFAULT_LIBDIR_GLOB = os.path.join(ROOT_DIR, 'lib', 'py', 'build', 'lib.*')
 
 
 class AbstractTest(unittest.TestCase):
@@ -90,11 +52,10 @@ class AbstractTest(unittest.TestCase):
       if options.zlib:
         self.transport = TZlibTransport.TZlibTransport(self.transport, 9)
     self.transport.open()
-    protocol = self.protocol_factory.getProtocol(self.transport)
+    protocol = self.get_protocol(self.transport)
     self.client = ThriftTest.Client(protocol)
 
   def tearDown(self):
-    # Close!
     self.transport.close()
 
   def testVoid(self):
@@ -190,7 +151,7 @@ class AbstractTest(unittest.TestCase):
 
   def testMap(self):
     print('testMap')
-    x = {0:1, 1:2, 2:3, 3:4, -1:-2}
+    x = {0: 1, 1: 2, 2: 3, 3: 4, -1: -2}
     y = self.client.testMap(x)
     self.assertEqual(y, x)
 
@@ -214,7 +175,7 @@ class AbstractTest(unittest.TestCase):
 
   def testTypedef(self):
     print('testTypedef')
-    x = 0xffffffffffffff # 7 bytes of 0xff
+    x = 0xffffffffffffff  # 7 bytes of 0xff
     y = self.client.testTypedef(x)
     self.assertEqual(y, x)
 
@@ -230,11 +191,11 @@ class AbstractTest(unittest.TestCase):
     print('testMulti')
     xpected = Xtruct(string_thing='Hello2', byte_thing=74, i32_thing=0xff00ff, i64_thing=0xffffffffd0d0)
     y = self.client.testMulti(xpected.byte_thing,
-          xpected.i32_thing,
-          xpected.i64_thing,
-          { 0:'abc' },
-          Numberz.FIVE,
-          0xf0f0f0)
+                              xpected.i32_thing,
+                              xpected.i64_thing,
+                              {0: 'abc'},
+                              Numberz.FIVE,
+                              0xf0f0f0)
     self.assertEqual(y, xpected)
 
   def testException(self):
@@ -248,8 +209,8 @@ class AbstractTest(unittest.TestCase):
       self.assertEqual(x.message, 'Xception')
       # TODO ensure same behavior for repr within generated python variants
       # ensure exception's repr method works
-      #x_repr = repr(x)
-      #self.assertEqual(x_repr, 'Xception(errorCode=1001, message=\'Xception\')')
+      # x_repr = repr(x)
+      # self.assertEqual(x_repr, 'Xception(errorCode=1001, message=\'Xception\')')
 
     try:
       self.client.testException('TException')
@@ -280,31 +241,35 @@ class AbstractTest(unittest.TestCase):
   def testOneway(self):
     print('testOneway')
     start = time.time()
-    self.client.testOneway(1) # type is int, not float
+    self.client.testOneway(1)  # type is int, not float
     end = time.time()
     self.assertTrue(end - start < 3,
                     "oneway sleep took %f sec" % (end - start))
 
   def testOnewayThenNormal(self):
     print('testOnewayThenNormal')
-    self.client.testOneway(1) # type is int, not float
+    self.client.testOneway(1)  # type is int, not float
     self.assertEqual(self.client.testString('Python'), 'Python')
 
 
 class NormalBinaryTest(AbstractTest):
-  protocol_factory = TBinaryProtocol.TBinaryProtocolFactory()
+  def get_protocol(self, transport):
+    return TBinaryProtocol.TBinaryProtocolFactory().getProtocol(transport)
 
 
 class CompactTest(AbstractTest):
-  protocol_factory = TCompactProtocol.TCompactProtocolFactory()
+  def get_protocol(self, transport):
+    return TCompactProtocol.TCompactProtocolFactory().getProtocol(transport)
 
 
 class JSONTest(AbstractTest):
-  protocol_factory = TJSONProtocol.TJSONProtocolFactory()
+  def get_protocol(self, transport):
+    return TJSONProtocol.TJSONProtocolFactory().getProtocol(transport)
 
 
 class AcceleratedBinaryTest(AbstractTest):
-  protocol_factory = TBinaryProtocol.TBinaryProtocolAcceleratedFactory()
+  def get_protocol(self, transport):
+    return TBinaryProtocol.TBinaryProtocolAcceleratedFactory().getProtocol(transport)
 
 
 def suite():
@@ -328,8 +293,54 @@ class OwnArgsTestProgram(unittest.TestProgram):
         if args:
             self.testNames = args
         else:
-            self.testNames = (self.defaultTest,)
+            self.testNames = ([self.defaultTest])
         self.createTests()
 
 if __name__ == "__main__":
+  parser = OptionParser()
+  parser.add_option('--libpydir', type='string', dest='libpydir',
+                    help='include this directory in sys.path for locating library code')
+  parser.add_option('--genpydir', type='string', dest='genpydir',
+                    default='gen-py',
+                    help='include this directory in sys.path for locating generated code')
+  parser.add_option("--port", type="int", dest="port",
+                    help="connect to server at port")
+  parser.add_option("--host", type="string", dest="host",
+                    help="connect to server")
+  parser.add_option("--zlib", action="store_true", dest="zlib",
+                    help="use zlib wrapper for compressed transport")
+  parser.add_option("--ssl", action="store_true", dest="ssl",
+                    help="use SSL for encrypted transport")
+  parser.add_option("--http", dest="http_path",
+                    help="Use the HTTP transport with the specified path")
+  parser.add_option('-v', '--verbose', action="store_const",
+                    dest="verbose", const=2,
+                    help="verbose output")
+  parser.add_option('-q', '--quiet', action="store_const",
+                    dest="verbose", const=0,
+                    help="minimal output")
+  parser.add_option('--protocol', dest="proto", type="string",
+                    help="protocol to use, one of: accel, binary, compact, json")
+  parser.add_option('--transport', dest="trans", type="string",
+                    help="transport to use, one of: buffered, framed")
+  parser.set_defaults(framed=False, http_path=None, verbose=1, host='localhost', port=9090, proto='binary')
+  options, args = parser.parse_args()
+
+  sys.path.insert(0, os.path.join(SCRIPT_DIR, options.genpydir))
+  if options.libpydir:
+    sys.path.insert(0, glob.glob(options.libpydir)[0])
+  else:
+    sys.path.insert(0, glob.glob(DEFAULT_LIBDIR_GLOB)[0])
+
+  from ThriftTest import ThriftTest
+  from ThriftTest.ttypes import Xtruct, Xtruct2, Numberz, Xception, Xception2
+  from thrift.Thrift import TException
+  from thrift.transport import TTransport
+  from thrift.transport import TSocket
+  from thrift.transport import THttpClient
+  from thrift.transport import TZlibTransport
+  from thrift.protocol import TBinaryProtocol
+  from thrift.protocol import TCompactProtocol
+  from thrift.protocol import TJSONProtocol
+
   OwnArgsTestProgram(defaultTest="suite", testRunner=unittest.TextTestRunner(verbosity=1))

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/test/py/TestServer.py
----------------------------------------------------------------------
diff --git a/test/py/TestServer.py b/test/py/TestServer.py
index bc221c0..4fa8894 100755
--- a/test/py/TestServer.py
+++ b/test/py/TestServer.py
@@ -26,55 +26,9 @@ import sys
 import time
 from optparse import OptionParser
 
-# Print TServer log to stdout so that the test-runner can redirect it to log files
-logging.basicConfig(level=logging.DEBUG)
-
-parser = OptionParser()
-parser.add_option('--genpydir', type='string', dest='genpydir',
-                  default='gen-py',
-                  help='include this local directory in sys.path for locating generated code')
-parser.add_option("--port", type="int", dest="port",
-    help="port number for server to listen on")
-parser.add_option("--zlib", action="store_true", dest="zlib",
-    help="use zlib wrapper for compressed transport")
-parser.add_option("--ssl", action="store_true", dest="ssl",
-    help="use SSL for encrypted transport")
-parser.add_option('-v', '--verbose', action="store_const",
-    dest="verbose", const=2,
-    help="verbose output")
-parser.add_option('-q', '--quiet', action="store_const",
-    dest="verbose", const=0,
-    help="minimal output")
-parser.add_option('--protocol',  dest="proto", type="string",
-    help="protocol to use, one of: accel, binary, compact, json")
-parser.add_option('--transport',  dest="trans", type="string",
-    help="transport to use, one of: buffered, framed")
-parser.set_defaults(port=9090, verbose=1, proto='binary')
-options, args = parser.parse_args()
-
-script_dir = os.path.realpath(os.path.dirname(__file__))  # <-- absolute dir the script is in
-lib_dir = os.path.join(os.path.dirname(os.path.dirname(script_dir)), 'lib', 'py', 'build', 'lib*')
-
-sys.path.insert(0, os.path.join(script_dir, options.genpydir))
-sys.path.insert(0, glob.glob(lib_dir)[0])
-
-from ThriftTest import ThriftTest
-from ThriftTest.ttypes import *
-from thrift.Thrift import TException
-from thrift.transport import TTransport
-from thrift.transport import TSocket
-from thrift.transport import TZlibTransport
-from thrift.protocol import TBinaryProtocol
-from thrift.protocol import TCompactProtocol
-from thrift.protocol import TJSONProtocol
-from thrift.server import TServer, TNonblockingServer, THttpServer
-
-PROT_FACTORIES = {
-    'binary': TBinaryProtocol.TBinaryProtocolFactory,
-    'accel': TBinaryProtocol.TBinaryProtocolAcceleratedFactory,
-    'compact': TCompactProtocol.TCompactProtocolFactory,
-    'json': TJSONProtocol.TJSONProtocolFactory,
-}
+SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
+ROOT_DIR = os.path.dirname(os.path.dirname(SCRIPT_DIR))
+DEFAULT_LIBDIR_GLOB = os.path.join(ROOT_DIR, 'lib', 'py', 'build', 'lib.*')
 
 
 class TestHandler(object):
@@ -224,78 +178,133 @@ class TestHandler(object):
                   byte_thing=arg0, i32_thing=arg1, i64_thing=arg2)
 
 
-# set up the protocol factory form the --protocol option
-pfactory_cls = PROT_FACTORIES.get(options.proto, None)
-if pfactory_cls is None:
-  raise AssertionError('Unknown --protocol option: %s' % options.proto)
-pfactory = pfactory_cls()
-
-# get the server type (TSimpleServer, TNonblockingServer, etc...)
-if len(args) > 1:
-  raise AssertionError('Only one server type may be specified, not multiple types.')
-server_type = args[0]
-
-# Set up the handler and processor objects
-handler   = TestHandler()
-processor = ThriftTest.Processor(handler)
-
-# Handle THttpServer as a special case
-if server_type == 'THttpServer':
-  server = THttpServer.THttpServer(processor, ('', options.port), pfactory)
-  server.serve()
-  sys.exit(0)
-
-# set up server transport and transport factory
+def main(options):
+  # Print TServer log to stdout so that the test-runner can redirect it to log files
+  logging.basicConfig(level=logging.DEBUG)
 
-abs_key_path = os.path.join(os.path.dirname(script_dir), 'keys', 'server.pem')
-
-host = None
-if options.ssl:
-  from thrift.transport import TSSLSocket
-  transport = TSSLSocket.TSSLServerSocket(host, options.port, certfile=abs_key_path)
-else:
-  transport = TSocket.TServerSocket(host, options.port)
-tfactory = TTransport.TBufferedTransportFactory()
-if options.trans == 'buffered':
-  tfactory = TTransport.TBufferedTransportFactory()
-elif options.trans == 'framed':
-  tfactory = TTransport.TFramedTransportFactory()
-elif options.trans == '':
-  raise AssertionError('Unknown --transport option: %s' % options.trans)
-else:
+  # set up the protocol factory form the --protocol option
+  prot_factories = {
+    'binary': TBinaryProtocol.TBinaryProtocolFactory,
+    'accel': TBinaryProtocol.TBinaryProtocolAcceleratedFactory,
+    'compact': TCompactProtocol.TCompactProtocolFactory,
+    'json': TJSONProtocol.TJSONProtocolFactory,
+  }
+  pfactory_cls = prot_factories.get(options.proto, None)
+  if pfactory_cls is None:
+    raise AssertionError('Unknown --protocol option: %s' % options.proto)
+  pfactory = pfactory_cls()
+
+  # get the server type (TSimpleServer, TNonblockingServer, etc...)
+  if len(args) > 1:
+    raise AssertionError('Only one server type may be specified, not multiple types.')
+  server_type = args[0]
+
+  # Set up the handler and processor objects
+  handler = TestHandler()
+  processor = ThriftTest.Processor(handler)
+
+  # Handle THttpServer as a special case
+  if server_type == 'THttpServer':
+    server = THttpServer.THttpServer(processor, ('', options.port), pfactory)
+    server.serve()
+    sys.exit(0)
+
+  # set up server transport and transport factory
+
+  abs_key_path = os.path.join(os.path.dirname(SCRIPT_DIR), 'keys', 'server.pem')
+
+  host = None
+  if options.ssl:
+    from thrift.transport import TSSLSocket
+    transport = TSSLSocket.TSSLServerSocket(host, options.port, certfile=abs_key_path)
+  else:
+    transport = TSocket.TServerSocket(host, options.port)
   tfactory = TTransport.TBufferedTransportFactory()
-# if --zlib, then wrap server transport, and use a different transport factory
-if options.zlib:
-  transport = TZlibTransport.TZlibTransport(transport)  # wrap  with zlib
-  tfactory = TZlibTransport.TZlibTransportFactory()
-
-# do server-specific setup here:
-if server_type == "TNonblockingServer":
-  server = TNonblockingServer.TNonblockingServer(processor, transport, inputProtocolFactory=pfactory)
-elif server_type == "TProcessPoolServer":
-  import signal
-  from thrift.server import TProcessPoolServer
-  server = TProcessPoolServer.TProcessPoolServer(processor, transport, tfactory, pfactory)
-  server.setNumWorkers(5)
-
-  def set_alarm():
-    def clean_shutdown(signum, frame):
-      for worker in server.workers:
+  if options.trans == 'buffered':
+    tfactory = TTransport.TBufferedTransportFactory()
+  elif options.trans == 'framed':
+    tfactory = TTransport.TFramedTransportFactory()
+  elif options.trans == '':
+    raise AssertionError('Unknown --transport option: %s' % options.trans)
+  else:
+    tfactory = TTransport.TBufferedTransportFactory()
+  # if --zlib, then wrap server transport, and use a different transport factory
+  if options.zlib:
+    transport = TZlibTransport.TZlibTransport(transport)  # wrap  with zlib
+    tfactory = TZlibTransport.TZlibTransportFactory()
+
+  # do server-specific setup here:
+  if server_type == "TNonblockingServer":
+    server = TNonblockingServer.TNonblockingServer(processor, transport, inputProtocolFactory=pfactory)
+  elif server_type == "TProcessPoolServer":
+    import signal
+    from thrift.server import TProcessPoolServer
+    server = TProcessPoolServer.TProcessPoolServer(processor, transport, tfactory, pfactory)
+    server.setNumWorkers(5)
+
+    def set_alarm():
+      def clean_shutdown(signum, frame):
+        for worker in server.workers:
+          if options.verbose > 0:
+            logging.info('Terminating worker: %s' % worker)
+          worker.terminate()
         if options.verbose > 0:
-          logging.info('Terminating worker: %s' % worker)
-        worker.terminate()
-      if options.verbose > 0:
-        logging.info('Requesting server to stop()')
-      try:
-        server.stop()
-      except:
-        pass
-    signal.signal(signal.SIGALRM, clean_shutdown)
-    signal.alarm(4)
-  set_alarm()
-else:
-  # look up server class dynamically to instantiate server
-  ServerClass = getattr(TServer, server_type)
-  server = ServerClass(processor, transport, tfactory, pfactory)
-# enter server main loop
-server.serve()
+          logging.info('Requesting server to stop()')
+        try:
+          server.stop()
+        except:
+          pass
+      signal.signal(signal.SIGALRM, clean_shutdown)
+      signal.alarm(4)
+    set_alarm()
+  else:
+    # look up server class dynamically to instantiate server
+    ServerClass = getattr(TServer, server_type)
+    server = ServerClass(processor, transport, tfactory, pfactory)
+  # enter server main loop
+  server.serve()
+
+if __name__ == '__main__':
+  parser = OptionParser()
+  parser.add_option('--libpydir', type='string', dest='libpydir',
+                    help='include this directory to sys.path for locating library code')
+  parser.add_option('--genpydir', type='string', dest='genpydir',
+                    default='gen-py',
+                    help='include this directory to sys.path for locating generated code')
+  parser.add_option("--port", type="int", dest="port",
+                    help="port number for server to listen on")
+  parser.add_option("--zlib", action="store_true", dest="zlib",
+                    help="use zlib wrapper for compressed transport")
+  parser.add_option("--ssl", action="store_true", dest="ssl",
+                    help="use SSL for encrypted transport")
+  parser.add_option('-v', '--verbose', action="store_const",
+                    dest="verbose", const=2,
+                    help="verbose output")
+  parser.add_option('-q', '--quiet', action="store_const",
+                    dest="verbose", const=0,
+                    help="minimal output")
+  parser.add_option('--protocol', dest="proto", type="string",
+                    help="protocol to use, one of: accel, binary, compact, json")
+  parser.add_option('--transport', dest="trans", type="string",
+                    help="transport to use, one of: buffered, framed")
+  parser.set_defaults(port=9090, verbose=1, proto='binary')
+  options, args = parser.parse_args()
+
+  sys.path.insert(0, os.path.join(SCRIPT_DIR, options.genpydir))
+  if options.libpydir:
+    sys.path.insert(0, glob.glob(options.libpydir)[0])
+  else:
+    sys.path.insert(0, glob.glob(DEFAULT_LIBDIR_GLOB)[0])
+
+  from ThriftTest import ThriftTest
+  from ThriftTest.ttypes import Xtruct, Xception, Xception2, Insanity
+  from thrift.Thrift import TException
+  from thrift.transport import TTransport
+  from thrift.transport import TSocket
+  from thrift.transport import TZlibTransport
+  from thrift.protocol import TBinaryProtocol
+  from thrift.protocol import TCompactProtocol
+  from thrift.protocol import TJSONProtocol
+  from thrift.server import TServer, TNonblockingServer, THttpServer
+
+  sys.exit(main(options))

http://git-wip-us.apache.org/repos/asf/thrift/blob/a185d7e7/test/tests.json
----------------------------------------------------------------------
diff --git a/test/tests.json b/test/tests.json
index 0c35df2..afc7531 100644
--- a/test/tests.json
+++ b/test/tests.json
@@ -202,6 +202,45 @@
     "workdir": "py"
   },
   {
+    "comment": "Using 'python3' executable to test py2 and 3 at once",
+    "name": "py3",
+    "server": {
+      "delay": 1,
+      "extra_args": ["TSimpleServer"],
+      "command": [
+        "python3",
+        "TestServer.py",
+        "--verbose",
+        "--libpydir=../../lib/py/build/lib",
+        "--genpydir=gen-py"
+      ]
+    },
+    "client": {
+      "timeout": 10,
+      "command": [
+        "python3",
+        "TestClient.py",
+        "--host=localhost",
+        "--libpydir=../../lib/py/build/lib",
+        "--genpydir=gen-py"
+      ]
+    },
+    "transports": [
+      "buffered",
+      "framed"
+    ],
+    "sockets": [
+      "ip-ssl",
+      "ip"
+    ],
+    "protocols": [
+      "compact",
+      "binary",
+      "json"
+    ],
+    "workdir": "py"
+  },
+  {
     "name": "cpp",
     "server": {
       "delay": 2,