1#!/usr/bin/env python
2
3#
4# Licensed to the Apache Software Foundation (ASF) under one
5# or more contributor license agreements. See the NOTICE file
6# distributed with this work for additional information
7# regarding copyright ownership. The ASF licenses this file
8# to you under the Apache License, Version 2.0 (the
9# "License"); you may not use this file except in compliance
10# with the License. You may obtain a copy of the License at
11#
12#   http://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing,
15# software distributed under the License is distributed on an
16# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17# KIND, either express or implied. See the License for the
18# specific language governing permissions and limitations
19# under the License.
20#
21
22from __future__ import division
23from __future__ import print_function
24import platform
25import copy
26import os
27import signal
28import socket
29import subprocess
30import sys
31import time
32from optparse import OptionParser
33
34from util import local_libpath
35
36SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
37
38SCRIPTS = [
39    'FastbinaryTest.py',
40    'TestFrozen.py',
41    'TestRenderedDoubleConstants.py',
42    'TSimpleJSONProtocolTest.py',
43    'SerializationTest.py',
44    'TestEof.py',
45    'TestSyntax.py',
46    'TestSocket.py',
47]
48FRAMED = ["TNonblockingServer"]
49SKIP_ZLIB = ['TNonblockingServer', 'THttpServer']
50SKIP_SSL = ['THttpServer']
51EXTRA_DELAY = dict(TProcessPoolServer=5.5)
52
53PROTOS = [
54    'accel',
55    'accelc',
56    'binary',
57    'compact',
58    'json',
59    'header',
60]
61
62
63def default_servers():
64    servers = [
65        'TSimpleServer',
66        'TThreadedServer',
67        'TThreadPoolServer',
68        'TNonblockingServer',
69        'THttpServer',
70    ]
71    if platform.system() != 'Windows':
72        servers.append('TProcessPoolServer')
73        servers.append('TForkingServer')
74    return servers
75
76
77def relfile(fname):
78    return os.path.join(SCRIPT_DIR, fname)
79
80
81def setup_pypath(libdir, gendir):
82    dirs = [libdir, gendir]
83    env = copy.deepcopy(os.environ)
84    pypath = env.get('PYTHONPATH', None)
85    if pypath:
86        dirs.append(pypath)
87    env['PYTHONPATH'] = os.pathsep.join(dirs)
88    if gendir.endswith('gen-py-no_utf8strings'):
89        env['THRIFT_TEST_PY_NO_UTF8STRINGS'] = '1'
90    return env
91
92
93def runScriptTest(libdir, genbase, genpydir, script):
94    env = setup_pypath(libdir, os.path.join(genbase, genpydir))
95    script_args = [sys.executable, relfile(script)]
96    print('\nTesting script: %s\n----' % (' '.join(script_args)))
97    ret = subprocess.call(script_args, env=env)
98    if ret != 0:
99        print('*** FAILED ***', file=sys.stderr)
100        print('LIBDIR: %s' % libdir, file=sys.stderr)
101        print('PY_GEN: %s' % genpydir, file=sys.stderr)
102        print('SCRIPT: %s' % script, file=sys.stderr)
103        raise Exception("Script subprocess failed, retcode=%d, args: %s" % (ret, ' '.join(script_args)))
104
105
106def runServiceTest(libdir, genbase, genpydir, server_class, proto, port, use_zlib, use_ssl, verbose):
107    env = setup_pypath(libdir, os.path.join(genbase, genpydir))
108    # Build command line arguments
109    server_args = [sys.executable, relfile('TestServer.py')]
110    cli_args = [sys.executable, relfile('TestClient.py')]
111    for which in (server_args, cli_args):
112        which.append('--protocol=%s' % proto)  # accel, binary, compact or json
113        which.append('--port=%d' % port)  # default to 9090
114        if use_zlib:
115            which.append('--zlib')
116        if use_ssl:
117            which.append('--ssl')
118        if verbose == 0:
119            which.append('-q')
120        if verbose == 2:
121            which.append('-v')
122    # server-specific option to select server class
123    server_args.append(server_class)
124    # client-specific cmdline options
125    if server_class in FRAMED:
126        cli_args.append('--transport=framed')
127    else:
128        cli_args.append('--transport=buffered')
129    if server_class == 'THttpServer':
130        cli_args.append('--http=/')
131    if verbose > 0:
132        print('Testing server %s: %s' % (server_class, ' '.join(server_args)))
133    serverproc = subprocess.Popen(server_args, env=env)
134
135    def ensureServerAlive():
136        if serverproc.poll() is not None:
137            print(('FAIL: Server process (%s) failed with retcode %d')
138                  % (' '.join(server_args), serverproc.returncode))
139            raise Exception('Server subprocess %s died, args: %s'
140                            % (server_class, ' '.join(server_args)))
141
142    # Wait for the server to start accepting connections on the given port.
143    sleep_time = 0.1  # Seconds
144    max_attempts = 100
145    attempt = 0
146    while True:
147        sock4 = socket.socket()
148        sock6 = socket.socket(socket.AF_INET6)
149        try:
150            if sock4.connect_ex(('127.0.0.1', port)) == 0 \
151                    or sock6.connect_ex(('::1', port)) == 0:
152                break
153            attempt += 1
154            if attempt >= max_attempts:
155                raise Exception("TestServer not ready on port %d after %.2f seconds"
156                                % (port, sleep_time * attempt))
157            ensureServerAlive()
158            time.sleep(sleep_time)
159        finally:
160            sock4.close()
161            sock6.close()
162
163    try:
164        if verbose > 0:
165            print('Testing client: %s' % (' '.join(cli_args)))
166        ret = subprocess.call(cli_args, env=env)
167        if ret != 0:
168            print('*** FAILED ***', file=sys.stderr)
169            print('LIBDIR: %s' % libdir, file=sys.stderr)
170            print('PY_GEN: %s' % genpydir, file=sys.stderr)
171            raise Exception("Client subprocess failed, retcode=%d, args: %s" % (ret, ' '.join(cli_args)))
172    finally:
173        # check that server didn't die
174        ensureServerAlive()
175        extra_sleep = EXTRA_DELAY.get(server_class, 0)
176        if extra_sleep > 0 and verbose > 0:
177            print('Giving %s (proto=%s,zlib=%s,ssl=%s) an extra %d seconds for child'
178                  'processes to terminate via alarm'
179                  % (server_class, proto, use_zlib, use_ssl, extra_sleep))
180            time.sleep(extra_sleep)
181        sig = signal.SIGKILL if platform.system() != 'Windows' else signal.SIGABRT
182        os.kill(serverproc.pid, sig)
183        serverproc.wait()
184
185
186class TestCases(object):
187    def __init__(self, genbase, libdir, port, gendirs, servers, verbose):
188        self.genbase = genbase
189        self.libdir = libdir
190        self.port = port
191        self.verbose = verbose
192        self.gendirs = gendirs
193        self.servers = servers
194
195    def default_conf(self):
196        return {
197            'gendir': self.gendirs[0],
198            'server': self.servers[0],
199            'proto': PROTOS[0],
200            'zlib': False,
201            'ssl': False,
202        }
203
204    def run(self, conf, test_count):
205        with_zlib = conf['zlib']
206        with_ssl = conf['ssl']
207        try_server = conf['server']
208        try_proto = conf['proto']
209        genpydir = conf['gendir']
210        # skip any servers that don't work with the Zlib transport
211        if with_zlib and try_server in SKIP_ZLIB:
212            return False
213        # skip any servers that don't work with SSL
214        if with_ssl and try_server in SKIP_SSL:
215            return False
216        if self.verbose > 0:
217            print('\nTest run #%d:  (includes %s) Server=%s,  Proto=%s,  zlib=%s,  SSL=%s'
218                  % (test_count, genpydir, try_server, try_proto, with_zlib, with_ssl))
219        runServiceTest(self.libdir, self.genbase, genpydir, try_server, try_proto, self.port, with_zlib, with_ssl, self.verbose)
220        if self.verbose > 0:
221            print('OK: Finished (includes %s)  %s / %s proto / zlib=%s / SSL=%s.   %d combinations tested.'
222                  % (genpydir, try_server, try_proto, with_zlib, with_ssl, test_count))
223        return True
224
225    def test_feature(self, name, values):
226        test_count = 0
227        conf = self.default_conf()
228        for try_server in values:
229            conf[name] = try_server
230            if self.run(conf, test_count):
231                test_count += 1
232        return test_count
233
234    def run_all_tests(self):
235        test_count = 0
236        for try_server in self.servers:
237            for genpydir in self.gendirs:
238                for try_proto in PROTOS:
239                    for with_zlib in (False, True):
240                        # skip any servers that don't work with the Zlib transport
241                        if with_zlib and try_server in SKIP_ZLIB:
242                            continue
243                        for with_ssl in (False, True):
244                            # skip any servers that don't work with SSL
245                            if with_ssl and try_server in SKIP_SSL:
246                                continue
247                            test_count += 1
248                            if self.verbose > 0:
249                                print('\nTest run #%d:  (includes %s) Server=%s,  Proto=%s,  zlib=%s,  SSL=%s'
250                                      % (test_count, genpydir, try_server, try_proto, with_zlib, with_ssl))
251                            runServiceTest(self.libdir, self.genbase, genpydir, try_server, try_proto, self.port, with_zlib, with_ssl)
252                            if self.verbose > 0:
253                                print('OK: Finished (includes %s)  %s / %s proto / zlib=%s / SSL=%s.   %d combinations tested.'
254                                      % (genpydir, try_server, try_proto, with_zlib, with_ssl, test_count))
255        return test_count
256
257
258def main():
259    parser = OptionParser()
260    parser.add_option('--all', action="store_true", dest='all')
261    parser.add_option('--genpydirs', type='string', dest='genpydirs',
262                      default='default,slots,oldstyle,no_utf8strings,dynamic,dynamicslots',
263                      help='directory extensions for generated code, used as suffixes for \"gen-py-*\" added sys.path for individual tests')
264    parser.add_option("--port", type="int", dest="port", default=9090,
265                      help="port number for server to listen on")
266    parser.add_option('-v', '--verbose', action="store_const",
267                      dest="verbose", const=2,
268                      help="verbose output")
269    parser.add_option('-q', '--quiet', action="store_const",
270                      dest="verbose", const=0,
271                      help="minimal output")
272    parser.add_option('-L', '--libdir', dest="libdir", default=local_libpath(),
273                      help="directory path that contains Thrift Python library")
274    parser.add_option('--gen-base', dest="gen_base", default=SCRIPT_DIR,
275                      help="directory path that contains Thrift Python library")
276    parser.set_defaults(verbose=1)
277    options, args = parser.parse_args()
278
279    generated_dirs = []
280    for gp_dir in options.genpydirs.split(','):
281        generated_dirs.append('gen-py-%s' % (gp_dir))
282
283    # commandline permits a single class name to be specified to override SERVERS=[...]
284    servers = default_servers()
285    if len(args) == 1:
286        if args[0] in servers:
287            servers = args
288        else:
289            print('Unavailable server type "%s", please choose one of: %s' % (args[0], servers))
290            sys.exit(0)
291
292    tests = TestCases(options.gen_base, options.libdir, options.port, generated_dirs, servers, options.verbose)
293
294    # run tests without a client/server first
295    print('----------------')
296    print(' Executing individual test scripts with various generated code directories')
297    print(' Directories to be tested: ' + ', '.join(generated_dirs))
298    print(' Scripts to be tested: ' + ', '.join(SCRIPTS))
299    print('----------------')
300    for genpydir in generated_dirs:
301        for script in SCRIPTS:
302            runScriptTest(options.libdir, options.gen_base, genpydir, script)
303
304    print('----------------')
305    print(' Executing Client/Server tests with various generated code directories')
306    print(' Servers to be tested: ' + ', '.join(servers))
307    print(' Directories to be tested: ' + ', '.join(generated_dirs))
308    print(' Protocols to be tested: ' + ', '.join(PROTOS))
309    print(' Options to be tested: ZLIB(yes/no), SSL(yes/no)')
310    print('----------------')
311
312    if options.all:
313        tests.run_all_tests()
314    else:
315        tests.test_feature('gendir', generated_dirs)
316        tests.test_feature('server', servers)
317        tests.test_feature('proto', PROTOS)
318        tests.test_feature('zlib', [False, True])
319        tests.test_feature('ssl', [False, True])
320
321
322if __name__ == '__main__':
323    sys.exit(main())
324