diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 88fa08d7..82f299b4 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -27,6 +27,7 @@ jobs: - '2.8' - '2.10' - '2.11' + - 'master' python: - '3.6' - '3.7' @@ -57,11 +58,45 @@ jobs: - name: Clone the connector uses: actions/checkout@v3 + - name: Setup tt + run: | + curl -L https://tarantool.io/release/2/installer.sh | sudo bash + sudo apt install -y tt + tt version + tt init + - name: Install tarantool ${{ matrix.tarantool }} + if: matrix.tarantool != 'master' uses: tarantool/setup-tarantool@v2 with: tarantool-version: ${{ matrix.tarantool }} + - name: Get Tarantool master latest commit + if: matrix.tarantool == 'master' + run: | + commit_hash=$(git ls-remote https://github.com/tarantool/tarantool.git --branch master | head -c 8) + echo "LATEST_COMMIT=${commit_hash}" >> $GITHUB_ENV + shell: bash + + - name: Cache Tarantool master + if: matrix.tarantool == 'master' + id: cache-latest + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/bin + ${{ github.workspace }}/include + key: cache-latest-${{ env.LATEST_COMMIT }} + + - name: Setup Tarantool master + if: matrix.tarantool == 'master' && steps.cache-latest.outputs.cache-hit != 'true' + run: | + tt install tarantool master + + - name: Add Tarantool master to PATH + if: matrix.tarantool == 'master' + run: echo "${GITHUB_WORKSPACE}/bin" >> $GITHUB_PATH + - name: Setup Python for tests uses: actions/setup-python@v4 with: @@ -86,8 +121,6 @@ jobs: - name: Install the crud module for testing purposes run: | - curl -L https://tarantool.io/release/2/installer.sh | bash - sudo apt install -y tt tt rocks install crud - name: Run tests diff --git a/CHANGELOG.md b/CHANGELOG.md index cf674622..f3bd49f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added +- The ability to connect to the Tarantool using an existing socket fd (#304). + ## 1.1.2 - 2023-09-20 ### Fixed diff --git a/tarantool/__init__.py b/tarantool/__init__.py index 91f80e10..d7e99358 100644 --- a/tarantool/__init__.py +++ b/tarantool/__init__.py @@ -51,7 +51,7 @@ __version__ = '0.0.0-dev' -def connect(host="localhost", port=33013, user=None, password=None, +def connect(host="localhost", port=33013, socket_fd=None, user=None, password=None, encoding=ENCODING_DEFAULT, transport=DEFAULT_TRANSPORT, ssl_key_file=DEFAULT_SSL_KEY_FILE, ssl_cert_file=DEFAULT_SSL_CERT_FILE, @@ -64,6 +64,8 @@ def connect(host="localhost", port=33013, user=None, password=None, :param port: Refer to :paramref:`~tarantool.Connection.params.port`. + :param socket_fd: Refer to :paramref:`~tarantool.Connection.params.socket_fd`. + :param user: Refer to :paramref:`~tarantool.Connection.params.user`. :param password: Refer to @@ -93,6 +95,7 @@ def connect(host="localhost", port=33013, user=None, password=None, """ return Connection(host, port, + socket_fd=socket_fd, user=user, password=password, socket_timeout=SOCKET_TIMEOUT, diff --git a/tarantool/connection.py b/tarantool/connection.py index 17b43b64..1284fa25 100644 --- a/tarantool/connection.py +++ b/tarantool/connection.py @@ -4,6 +4,7 @@ # pylint: disable=too-many-lines,duplicate-code import os +import select import time import errno from enum import Enum @@ -51,6 +52,9 @@ RECONNECT_DELAY, DEFAULT_TRANSPORT, SSL_TRANSPORT, + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_SOCKET_FD, DEFAULT_SSL_KEY_FILE, DEFAULT_SSL_CERT_FILE, DEFAULT_SSL_CA_FILE, @@ -594,7 +598,10 @@ class Connection(ConnectionInterface): :value: :exc:`~tarantool.error.CrudModuleError` """ - def __init__(self, host, port, + def __init__(self, + host=DEFAULT_HOST, + port=DEFAULT_PORT, + socket_fd=DEFAULT_SOCKET_FD, user=None, password=None, socket_timeout=SOCKET_TIMEOUT, @@ -623,8 +630,11 @@ def __init__(self, host, port, Unix sockets. :type host: :obj:`str` or :obj:`None` - :param port: Server port or Unix socket path. - :type port: :obj:`int` or :obj:`str` + :param port: Server port, or Unix socket path. + :type port: :obj:`int` or :obj:`str` or :obj:`None` + + :param socket_fd: socket fd number. + :type socket_fd: :obj:`int` or :obj:`None` :param user: User name for authentication on the Tarantool server. @@ -804,6 +814,18 @@ def __init__(self, host, port, """ # pylint: disable=too-many-arguments,too-many-locals,too-many-statements + if host is None and port is None and socket_fd is None: + raise ConfigurationError("need to specify host/port, " + "port (in case of Unix sockets) " + "or socket_fd") + + if socket_fd is not None and (host is not None or port is not None): + raise ConfigurationError("specifying both socket_fd and host/port is not allowed") + + if host is not None and port is None: + raise ConfigurationError("when specifying host, " + "it is also necessary to specify port") + if msgpack.version >= (1, 0, 0) and encoding not in (None, 'utf-8'): raise ConfigurationError("msgpack>=1.0.0 only supports None and " + "'utf-8' encoding option values") @@ -820,6 +842,7 @@ def __init__(self, host, port, recv.restype = ctypes.c_int self.host = host self.port = port + self.socket_fd = socket_fd self.user = user self.password = password self.socket_timeout = socket_timeout @@ -897,10 +920,37 @@ def connect_basic(self): :meta private: """ - if self.host is None: - self.connect_unix() - else: + if self.socket_fd is not None: + self.connect_socket_fd() + elif self.host is not None: self.connect_tcp() + else: + self.connect_unix() + + def connect_socket_fd(self): + """ + Establish a connection using an existing socket fd. + + +---------------------+--------------------------+-------------------------+ + | socket_fd / timeout | >= 0 | `None` | + +=====================+==========================+=========================+ + | blocking | Set non-blocking socket | Don't change, `select` | + | | lib call `select` | isn't needed | + +---------------------+--------------------------+-------------------------+ + | non-blocking | Don't change, socket lib | Don't change, call | + | | call `select` | `select` ourselves | + +---------------------+--------------------------+-------------------------+ + + :meta private: + """ + + self.connected = True + if self._socket: + self._socket.close() + + self._socket = socket.socket(fileno=self.socket_fd) + if self.socket_timeout is not None: + self._socket.settimeout(self.socket_timeout) def connect_tcp(self): """ @@ -1124,6 +1174,11 @@ def _recv(self, to_read): while to_read > 0: try: tmp = self._socket.recv(to_read) + except BlockingIOError: + ready, _, _ = select.select([self._socket.fileno()], [], [], self.socket_timeout) + if not ready: + raise NetworkError(TimeoutError()) # pylint: disable=raise-missing-from + continue except OverflowError as exc: self._socket.close() err = socket.error( @@ -1163,6 +1218,41 @@ def _read_response(self): # Read the packet return self._recv(length) + def _sendall(self, bytes_to_send): + """ + Sends bytes to the transport (socket). + + :param bytes_to_send: Message to send. + :type bytes_to_send: :obj:`bytes` + + :raise: :exc:`~tarantool.error.NetworkError` + + :meta private: + """ + + total_sent = 0 + while total_sent < len(bytes_to_send): + try: + sent = self._socket.send(bytes_to_send[total_sent:]) + if sent == 0: + err = socket.error( + errno.ECONNRESET, + "Lost connection to server during query" + ) + raise NetworkError(err) + total_sent += sent + except BlockingIOError as exc: + total_sent += exc.characters_written + _, ready, _ = select.select([], [self._socket.fileno()], [], self.socket_timeout) + if not ready: + raise NetworkError(TimeoutError()) # pylint: disable=raise-missing-from + except socket.error as exc: + err = socket.error( + errno.ECONNRESET, + "Lost connection to server during query" + ) + raise NetworkError(err) from exc + def _send_request_wo_reconnect(self, request, on_push=None, on_push_ctx=None): """ Send request without trying to reconnect. @@ -1191,7 +1281,7 @@ def _send_request_wo_reconnect(self, request, on_push=None, on_push_ctx=None): response = None while True: try: - self._socket.sendall(bytes(request)) + self._sendall(bytes(request)) response = request.response_class(self, self._read_response()) break except SchemaReloadException as exc: diff --git a/tarantool/connection_pool.py b/tarantool/connection_pool.py index 3c1e72ac..7bbd4e16 100644 --- a/tarantool/connection_pool.py +++ b/tarantool/connection_pool.py @@ -115,7 +115,7 @@ class PoolUnit(): addr: dict """ - ``{"host": host, "port": port}`` info. + ``{"host": host, "port": port, "socket_fd": socket_fd}`` info. :type: :obj:`dict` """ @@ -161,6 +161,14 @@ class PoolUnit(): :type: :obj:`bool` """ + def get_address(self): + """ + Get an address string representation. + """ + if self.addr['socket_fd'] is not None: + return f'fd://{self.addr["socket_fd"]}' + return f'{self.addr["host"]}:{self.addr["port"]}' + # Based on https://realpython.com/python-interface/ class StrategyInterface(metaclass=abc.ABCMeta): @@ -398,6 +406,7 @@ def __init__(self, { "host': "str" or None, # mandatory "port": int or "str", # mandatory + "socket_fd": int, # optional "transport": "str", # optional "ssl_key_file": "str", # optional "ssl_cert_file": "str", # optional @@ -499,6 +508,7 @@ def __init__(self, conn=Connection( host=addr['host'], port=addr['port'], + socket_fd=addr['socket_fd'], user=user, password=password, socket_timeout=socket_timeout, @@ -529,15 +539,16 @@ def _make_key(self, addr): """ Make a unique key for a server based on its address. - :param addr: `{"host": host, "port": port}` dictionary. + :param addr: `{"host": host, "port": port, "socket_fd": socket_fd}` dictionary. :type addr: :obj:`dict` :rtype: :obj:`str` :meta private: """ - - return f"{addr['host']}:{addr['port']}" + if addr['socket_fd'] is None: + return f"{addr['host']}:{addr['port']}" + return addr['socket_fd'] def _get_new_state(self, unit): """ @@ -557,7 +568,7 @@ def _get_new_state(self, unit): try: conn.connect() except NetworkError as exc: - msg = (f"Failed to connect to {unit.addr['host']}:{unit.addr['port']}, " + msg = (f"Failed to connect to {unit.get_address()}, " f"reason: {repr(exc)}") warn(msg, ClusterConnectWarning) return InstanceState(Status.UNHEALTHY) @@ -565,7 +576,7 @@ def _get_new_state(self, unit): try: resp = conn.call('box.info') except NetworkError as exc: - msg = (f"Failed to get box.info for {unit.addr['host']}:{unit.addr['port']}, " + msg = (f"Failed to get box.info for {unit.get_address()}, " f"reason: {repr(exc)}") warn(msg, PoolTolopogyWarning) return InstanceState(Status.UNHEALTHY) @@ -573,7 +584,7 @@ def _get_new_state(self, unit): try: read_only = resp.data[0]['ro'] except (IndexError, KeyError) as exc: - msg = (f"Incorrect box.info response from {unit.addr['host']}:{unit.addr['port']}" + msg = (f"Incorrect box.info response from {unit.get_address()}" f"reason: {repr(exc)}") warn(msg, PoolTolopogyWarning) return InstanceState(Status.UNHEALTHY) @@ -582,11 +593,11 @@ def _get_new_state(self, unit): status = resp.data[0]['status'] if status != 'running': - msg = f"{unit.addr['host']}:{unit.addr['port']} instance status is not 'running'" + msg = f"{unit.get_address()} instance status is not 'running'" warn(msg, PoolTolopogyWarning) return InstanceState(Status.UNHEALTHY) except (IndexError, KeyError) as exc: - msg = (f"Incorrect box.info response from {unit.addr['host']}:{unit.addr['port']}" + msg = (f"Incorrect box.info response from {unit.get_address()}" f"reason: {repr(exc)}") warn(msg, PoolTolopogyWarning) return InstanceState(Status.UNHEALTHY) diff --git a/tarantool/const.py b/tarantool/const.py index 1e2b0895..53749cec 100644 --- a/tarantool/const.py +++ b/tarantool/const.py @@ -103,6 +103,12 @@ IPROTO_FEATURE_SPACE_AND_INDEX_NAMES = 5 IPROTO_FEATURE_WATCH_ONCE = 6 +# Default value for host. +DEFAULT_HOST = None +# Default value for port. +DEFAULT_PORT = None +# Default value for socket_fd. +DEFAULT_SOCKET_FD = None # Default value for connection timeout (seconds) CONNECTION_TIMEOUT = None # Default value for socket timeout (seconds) diff --git a/tarantool/mesh_connection.py b/tarantool/mesh_connection.py index ebbc1a40..0f86c0dd 100644 --- a/tarantool/mesh_connection.py +++ b/tarantool/mesh_connection.py @@ -28,6 +28,9 @@ DEFAULT_SSL_PASSWORD, DEFAULT_SSL_PASSWORD_FILE, CLUSTER_DISCOVERY_DELAY, + DEFAULT_HOST, + DEFAULT_SOCKET_FD, + DEFAULT_PORT, ) from tarantool.request import ( @@ -35,6 +38,9 @@ ) default_addr_opts = { + 'host': DEFAULT_HOST, + 'port': DEFAULT_PORT, + 'socket_fd': DEFAULT_SOCKET_FD, 'transport': DEFAULT_TRANSPORT, 'ssl_key_file': DEFAULT_SSL_KEY_FILE, 'ssl_cert_file': DEFAULT_SSL_CERT_FILE, @@ -91,7 +97,8 @@ def parse_error(uri, msg): return parse_error(uri, 'port should be a number') for key, val in default_addr_opts.items(): - result[key] = val + if key not in result: + result[key] = val if opts_str != "": for opt_str in opts_str.split('&'): @@ -127,9 +134,6 @@ def format_error(address, err): if not isinstance(address, dict): return format_error(address, 'address must be a dict') - if 'port' not in address or address['port'] is None: - return format_error(address, 'port is not set or None') - result = {} for key, val in address.items(): result[key] = val @@ -138,6 +142,17 @@ def format_error(address, err): if key not in result: result[key] = val + if result['socket_fd'] is not None: + # Looks like socket fd. + if result['host'] is not None or result['port'] is not None: + return format_error(result, + "specifying both socket_fd and host/port is not allowed") + + if not isinstance(result['socket_fd'], int): + return format_error(result, + 'socket_fd must be an int') + return result, None + if isinstance(result['port'], int): # Looks like an inet address. @@ -192,6 +207,7 @@ def update_connection(conn, address): conn.host = address["host"] conn.port = address["port"] + conn.socket_fd = address["socket_fd"] conn.transport = address['transport'] conn.ssl_key_file = address['ssl_key_file'] conn.ssl_cert_file = address['ssl_cert_file'] @@ -268,7 +284,10 @@ class MeshConnection(Connection): Represents a connection to a cluster of Tarantool servers. """ - def __init__(self, host=None, port=None, + def __init__(self, + host=DEFAULT_HOST, + port=DEFAULT_PORT, + socket_fd=DEFAULT_SOCKET_FD, user=None, password=None, socket_timeout=SOCKET_TIMEOUT, @@ -298,7 +317,12 @@ def __init__(self, host=None, port=None, :paramref:`~tarantool.MeshConnection.params.addrs` list. :param port: Refer to - :paramref:`~tarantool.Connection.params.host`. + :paramref:`~tarantool.Connection.params.port`. + Value would be used to add one more server in + :paramref:`~tarantool.MeshConnection.params.addrs` list. + + :param socket_fd: Refer to + :paramref:`~tarantol.Connection.params.socket_fd`. Value would be used to add one more server in :paramref:`~tarantool.MeshConnection.params.addrs` list. @@ -447,9 +471,10 @@ def __init__(self, host=None, port=None, # Don't change user provided arguments. addrs = addrs[:] - if host and port: + if (host and port) or socket_fd: addrs.insert(0, {'host': host, 'port': port, + 'socket_fd': socket_fd, 'transport': transport, 'ssl_key_file': ssl_key_file, 'ssl_cert_file': ssl_cert_file, @@ -484,6 +509,7 @@ def __init__(self, host=None, port=None, super().__init__( host=addr['host'], port=addr['port'], + socket_fd=addr['socket_fd'], user=user, password=password, socket_timeout=socket_timeout, @@ -604,6 +630,7 @@ def _opt_refresh_instances(self): # an instance list and connect to one of new instances. current_addr = {'host': self.host, 'port': self.port, + 'socket_fd': self.socket_fd, 'transport': self.transport, 'ssl_key_file': self.ssl_key_file, 'ssl_cert_file': self.ssl_cert_file, diff --git a/test/suites/__init__.py b/test/suites/__init__.py index d56b2889..7d092585 100644 --- a/test/suites/__init__.py +++ b/test/suites/__init__.py @@ -15,6 +15,7 @@ from .test_execute import TestSuiteExecute from .test_dbapi import TestSuiteDBAPI from .test_encoding import TestSuiteEncoding +from .test_socket_fd import TestSuiteSocketFD from .test_ssl import TestSuiteSsl from .test_decimal import TestSuiteDecimal from .test_uuid import TestSuiteUUID @@ -33,7 +34,7 @@ TestSuiteEncoding, TestSuitePool, TestSuiteSsl, TestSuiteDecimal, TestSuiteUUID, TestSuiteDatetime, TestSuiteInterval, TestSuitePackage, TestSuiteErrorExt, - TestSuitePush, TestSuiteConnection, TestSuiteCrud,) + TestSuitePush, TestSuiteConnection, TestSuiteCrud, TestSuiteSocketFD) def load_tests(loader, tests, pattern): diff --git a/test/suites/lib/skip.py b/test/suites/lib/skip.py index 00b1a21d..625caf6a 100644 --- a/test/suites/lib/skip.py +++ b/test/suites/lib/skip.py @@ -306,3 +306,14 @@ def skip_or_run_iproto_basic_features_test(func): return skip_or_run_test_tarantool(func, '2.10.0', 'does not support iproto ID and iproto basic features') + + +def skip_or_run_box_session_new_tests(func): + """ + Decorator to skip or run tests that use box.session.new. + + Tarantool supports box.session.new only in current master since + commit 324872a. + See https://github.com/tarantool/tarantool/issues/8801. + """ + return skip_or_run_test_tarantool(func, '3.0.0', 'does not support box.session.new') diff --git a/test/suites/sidecar.py b/test/suites/sidecar.py new file mode 100644 index 00000000..6bee5af7 --- /dev/null +++ b/test/suites/sidecar.py @@ -0,0 +1,16 @@ +# pylint: disable=missing-module-docstring +import os + +import tarantool + +socket_fd = int(os.environ["SOCKET_FD"]) + +conn = tarantool.connect(None, None, socket_fd=socket_fd) + +# Check user. +assert conn.eval("return box.session.user()").data[0] == "test" + +# Check db operations. +conn.insert("test", [1]) +conn.insert("test", [2]) +assert conn.select("test").data == [[1], [2]] diff --git a/test/suites/test_mesh.py b/test/suites/test_mesh.py index 606cc7d1..a906597b 100644 --- a/test/suites/test_mesh.py +++ b/test/suites/test_mesh.py @@ -135,7 +135,6 @@ def test_01_contructor(self): # Verify that a bad address given at initialization leads # to an error. bad_addrs = [ - {"port": 1234}, # no host {"host": "localhost"}, # no port {"host": "localhost", "port": "1234"}, # port is str ] diff --git a/test/suites/test_socket_fd.py b/test/suites/test_socket_fd.py new file mode 100644 index 00000000..5cf94777 --- /dev/null +++ b/test/suites/test_socket_fd.py @@ -0,0 +1,200 @@ +""" +This module tests work with connection over socket fd. +""" +import os.path +# pylint: disable=missing-class-docstring,missing-function-docstring + +import socket +import sys +import unittest + +import tarantool +from .lib.skip import skip_or_run_box_session_new_tests +from .lib.tarantool_server import TarantoolServer, find_port +from .utils import assert_admin_success + + +def find_python(): + for _dir in os.environ["PATH"].split(os.pathsep): + exe = os.path.join(_dir, "python") + if os.access(exe, os.X_OK): + return os.path.abspath(exe) + raise RuntimeError("Can't find python executable in " + os.environ["PATH"]) + + +class TestSuiteSocketFD(unittest.TestCase): + EVAL_USER = "return box.session.user()" + + @classmethod + def setUpClass(cls): + print(' SOCKET FD '.center(70, '='), file=sys.stderr) + print('-' * 70, file=sys.stderr) + + cls.srv = TarantoolServer() + cls.srv.script = 'test/suites/box.lua' + cls.srv.start() + cls.tcp_port = find_port() + + # Start tcp server to test work with blocking sockets. + # pylint: disable=consider-using-f-string + resp = cls.srv.admin(""" + local socket = require('socket') + + box.cfg{} + box.schema.user.create('test', {password = 'test', if_not_exists = true}) + box.schema.user.grant('test', 'read,write,execute,create', 'universe', + nil, {if_not_exists = true}) + box.schema.user.grant('guest', 'execute', 'universe', + nil, {if_not_exists = true}) + + socket.tcp_server('0.0.0.0', %d, function(s) + if not s:nonblock(true) then + s:close() + return + end + box.session.new({ + type = 'binary', + fd = s:fd(), + user = 'test', + }) + s:detach() + end) + + box.schema.create_space('test', { + format = {{type='unsigned', name='id'}}, + if_not_exists = true, + }) + box.space.test:create_index('primary') + + return true + """ % cls.tcp_port) + assert_admin_success(resp) + + @skip_or_run_box_session_new_tests + def setUp(self): + # Prevent a remote tarantool from clean our session. + if self.srv.is_started(): + self.srv.touch_lock() + + def _get_tt_sock(self): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self.srv.host, self.tcp_port)) + return sock + + def test_01_incorrect_params(self): + cases = { + "host and socket_fd": { + "args": {"host": "123", "socket_fd": 3}, + "msg": "specifying both socket_fd and host/port is not allowed", + }, + "port and socket_fd": { + "args": {"port": 14, "socket_fd": 3}, + "msg": "specifying both socket_fd and host/port is not allowed", + }, + "empty": { + "args": {}, + "msg": r"host/port.* port.* or socket_fd", + }, + "only host": { + "args": {"host": "localhost"}, + "msg": "when specifying host, it is also necessary to specify port", + }, + } + + for name, case in cases.items(): + with self.subTest(msg=name): + with self.assertRaisesRegex(tarantool.Error, case["msg"]): + tarantool.Connection(**case["args"]) + + def test_02_socket_fd_connect(self): + sock = self._get_tt_sock() + conn = tarantool.connect(None, None, socket_fd=sock.fileno()) + sock.detach() + try: + self.assertSequenceEqual(conn.eval(self.EVAL_USER), ["test"]) + finally: + conn.close() + + def test_03_socket_fd_re_auth(self): + sock = self._get_tt_sock() + conn = tarantool.connect(None, None, socket_fd=sock.fileno(), user="guest") + sock.detach() + try: + self.assertSequenceEqual(conn.eval(self.EVAL_USER), ["guest"]) + finally: + conn.close() + + @unittest.skipIf(sys.platform.startswith("win"), + "Skip on Windows since it uses remote server") + def test_04_tarantool_made_socket(self): + python_exe = find_python() + cwd = os.getcwd() + side_script_path = os.path.join(cwd, "test", "suites", "sidecar.py") + + # pylint: disable=consider-using-f-string + ret_code, err = self.srv.admin(""" + local socket = require('socket') + local popen = require('popen') + local os = require('os') + local s1, s2 = socket.socketpair('AF_UNIX', 'SOCK_STREAM', 0) + + --[[ Tell sidecar which fd use to connect. --]] + os.setenv('SOCKET_FD', tostring(s2:fd())) + + --[[ Tell sidecar where find `tarantool` module. --]] + os.setenv('PYTHONPATH', (os.getenv('PYTHONPATH') or '') .. ':' .. '%s') + + box.session.new({ + type = 'binary', + fd = s1:fd(), + user = 'test', + }) + s1:detach() + + local ph, err = popen.new({'%s', '%s'}, { + stdout = popen.opts.PIPE, + stderr = popen.opts.PIPE, + inherit_fds = {s2:fd()}, + }) + + if err ~= nil then + return 1, err + end + + ph:wait() + + local status_code = ph:info().status.exit_code + local stderr = ph:read({stderr=true}):rstrip() + return status_code, stderr + """ % (cwd, python_exe, side_script_path)) + self.assertIsNone(err, err) + self.assertEqual(ret_code, 0) + + def test_05_socket_fd_pool(self): + sock = self._get_tt_sock() + pool = tarantool.ConnectionPool( + addrs=[{'host': None, 'port': None, 'socket_fd': sock.fileno()}] + ) + sock.detach() + try: + self.assertSequenceEqual(pool.eval(self.EVAL_USER, mode=tarantool.Mode.ANY), ["test"]) + finally: + pool.close() + + def test_06_socket_fd_mesh(self): + sock = self._get_tt_sock() + mesh = tarantool.MeshConnection( + host=None, + port=None, + socket_fd=sock.fileno() + ) + sock.detach() + try: + self.assertSequenceEqual(mesh.eval(self.EVAL_USER), ["test"]) + finally: + mesh.close() + + @classmethod + def tearDownClass(cls): + cls.srv.stop() + cls.srv.clean()