• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tarantool / test-run / 9259128115

27 May 2024 06:47PM UTC coverage: 62.506% (-0.03%) from 62.54%
9259128115

push

github

ylobankov
flake8: fix E721 do not compare types

https://www.flake8rules.com/rules/E721.html

759 of 1564 branches covered (48.53%)

Branch coverage included in aggregate %.

2 of 4 new or added lines in 2 files covered. (50.0%)

1 existing line in 1 file now uncovered.

2937 of 4349 relevant lines covered (67.53%)

0.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.9
/lib/tarantool_server.py
1
import errno
1✔
2
import gevent
1✔
3
import glob
1✔
4
import inspect  # for caller_globals
1✔
5
import os
1✔
6
import os.path
1✔
7
import re
1✔
8
import shlex
1✔
9
import shutil
1✔
10
import signal
1✔
11
import subprocess
1✔
12
import sys
1✔
13
import textwrap
1✔
14
import time
1✔
15
import yaml
1✔
16

17
from gevent import socket
1✔
18
from gevent import Timeout
1✔
19
from greenlet import GreenletExit
1✔
20
from threading import Timer
1✔
21

22
try:
1✔
23
    # Python 2
24
    from StringIO import StringIO
1✔
25
except ImportError:
1✔
26
    # Python 3
27
    from io import StringIO
1✔
28

29
from lib.admin_connection import AdminConnection, AdminAsyncConnection, BrokenConsoleHandshake
1✔
30
from lib.box_connection import BoxConnection
1✔
31
from lib.colorer import color_stdout
1✔
32
from lib.colorer import color_log
1✔
33
from lib.colorer import qa_notice
1✔
34
from lib.options import Options
1✔
35
from lib.preprocessor import TestState
1✔
36
from lib.sampler import sampler
1✔
37
from lib.server import Server
1✔
38
from lib.server import DEFAULT_SNAPSHOT_NAME
1✔
39
from lib.test import Test
1✔
40
from lib.utils import bytes_to_str
1✔
41
from lib.utils import extract_schema_from_snapshot
1✔
42
from lib.utils import format_process
1✔
43
from lib.utils import safe_makedirs
1✔
44
from lib.utils import signame
1✔
45
from lib.utils import warn_unix_socket
1✔
46
from lib.utils import prefix_each_line
1✔
47
from lib.utils import prepend_path
1✔
48
from lib.utils import PY3
1✔
49
from lib.test import TestRunGreenlet, TestExecutionError
1✔
50

51

52
def save_join(green_obj, timeout=None):
1✔
53
    """
54
    Gevent join wrapper for
55
    test-run stop-on-crash/stop-on-timeout feature
56
    """
57
    try:
1✔
58
        green_obj.get(timeout=timeout)
1✔
59
    except Timeout:
×
60
        color_stdout("Test timeout of %d secs reached\t" % timeout, schema='error')
×
61
        # We should kill the greenlet that writes to a temporary
62
        # result file. If the same test is run several times (e.g.
63
        # on different configurations), this greenlet may wake up
64
        # and write to the temporary result file of the new run of
65
        # the test.
66
        green_obj.kill()
×
67
    except GreenletExit:
×
68
        pass
×
69
    # We don't catch TarantoolStartError here to propagate it to a parent
70
    # greenlet to report a (default or non-default) tarantool server fail.
71

72

73
class LuaTest(Test):
1✔
74
    """ Handle *.test.lua and *.test.sql test files. """
75

76
    RESULT_FILE_VERSION_INITIAL = 1
1✔
77
    RESULT_FILE_VERSION_DEFAULT = 2
1✔
78
    RESULT_FILE_VERSION_LINE_RE = re.compile(
1✔
79
        r'^-- test-run result file version (?P<version>\d+)$')
80
    RESULT_FILE_VERSION_TEMPLATE = '-- test-run result file version {}'
1✔
81
    TAGS_LINE_RE = re.compile(r'^-- tags:')
1✔
82

83
    def __init__(self, *args, **kwargs):
1✔
84
        super(LuaTest, self).__init__(*args, **kwargs)
1✔
85
        if self.name.endswith('.test.lua'):
1✔
86
            self.default_language = 'lua'
1✔
87
        else:
88
            assert self.name.endswith('.test.sql')
1✔
89
            self.default_language = 'sql'
1✔
90
        self.result_file_version = self.result_file_version()
1✔
91

92
    def result_file_version(self):
1✔
93
        """ If a result file is not exists, return a default
94
            version (last known by test-run).
95
            If it exists, but does not contain a valid result file
96
            header, return 1.
97
            If it contains a version, return the version.
98
        """
99
        if not os.path.isfile(self.result):
1!
100
            return self.RESULT_FILE_VERSION_DEFAULT
×
101

102
        with open(self.result, 'r') as f:
1✔
103
            line = f.readline().rstrip('\n')
1✔
104

105
            # An empty line or EOF.
106
            if not line:
1!
107
                return self.RESULT_FILE_VERSION_INITIAL
×
108

109
            # No result file header.
110
            m = self.RESULT_FILE_VERSION_LINE_RE.match(line)
1✔
111
            if not m:
1!
112
                return self.RESULT_FILE_VERSION_INITIAL
×
113

114
            # A version should be integer.
115
            try:
1✔
116
                return int(m.group('version'))
1✔
117
            except ValueError:
×
118
                return self.RESULT_FILE_VERSION_INITIAL
×
119

120
    def write_result_file_version_line(self):
1✔
121
        # The initial version of a result file does not have a
122
        # version line.
123
        if self.result_file_version < 2:
1!
124
            return
×
125
        sys.stdout.write(self.RESULT_FILE_VERSION_TEMPLATE.format(
1✔
126
                         self.result_file_version) + '\n')
127

128
    def execute_pragma_sql_default_engine(self, ts):
1✔
129
        """ Set default engine for an SQL test if it is provided
130
            in a configuration.
131

132
            Return True if the command is successful or when it is
133
            not performed, otherwise (when got an unexpected
134
            result for the command) return False.
135
        """
136
        # Pass the command only for *.test.sql test files, because
137
        # hence we sure tarantool supports SQL.
138
        if self.default_language != 'sql':
1✔
139
            return True
1✔
140

141
        # Skip if no 'memtx' or 'vinyl' engine is provided.
142
        ok = self.run_params and 'engine' in self.run_params and \
1✔
143
            self.run_params['engine'] in ('memtx', 'vinyl')
144
        if not ok:
1!
145
            return True
×
146

147
        engine = self.run_params['engine']
1✔
148

149
        # Probe the new way. Pass through on any error.
150
        command_new = ("UPDATE \"_session_settings\" SET \"value\" = '{}' " +
1✔
151
                       "WHERE \"name\" = 'sql_default_engine'").format(engine)
152
        result_new = self.send_command(command_new, ts, 'sql')
1✔
153
        result_new = result_new.replace('\r\n', '\n')
1✔
154
        if result_new == '---\n- row_count: 1\n...\n':
1!
155
            return True
1✔
156

157
        # Probe the old way. Fail the test on an error.
158
        command_old = "pragma sql_default_engine='{}'".format(engine)
×
159
        result_old = self.send_command(command_old, ts, 'sql')
×
160
        result_old = result_old.replace('\r\n', '\n')
×
161
        if result_old == '---\n- row_count: 0\n...\n':
×
162
            return True
×
163

164
        sys.stdout.write(command_new)
×
165
        sys.stdout.write(result_new)
×
166
        sys.stdout.write(command_old)
×
167
        sys.stdout.write(result_old)
×
168
        return False
×
169

170
    def send_command_raw(self, command, ts):
1✔
171
        """ Send a command to tarantool and read a response. """
172
        color_log('DEBUG: sending command: {}\n'.format(command.rstrip()),
1✔
173
                  schema='tarantool command')
174
        # Evaluate the request on the first connection, save the
175
        # response.
176
        result = ts.curcon[0](command, silent=True)
1✔
177
        # Evaluate on other connections, ignore responses.
178
        for conn in ts.curcon[1:]:
1!
179
            conn(command, silent=True)
×
180
        # gh-24 fix
181
        if result is None:
1!
182
            result = '[Lost current connection]\n'
×
183
        color_log("DEBUG: tarantool's response for [{}]\n{}\n".format(
1✔
184
            command.rstrip(), prefix_each_line(' | ', result)),
185
            schema='tarantool command')
186
        return result
1✔
187

188
    def set_language(self, ts, language):
1✔
189
        command = r'\set language ' + language
1✔
190
        self.send_command_raw(command, ts)
1✔
191

192
    def send_command(self, command, ts, language=None):
1✔
193
        if language:
1✔
194
            self.set_language(ts, language)
1✔
195
        return self.send_command_raw(command, ts)
1✔
196

197
    def flush(self, ts, command_log, command_exe):
1✔
198
        # Write a command to a result file.
199
        command = command_log.getvalue()
1✔
200
        sys.stdout.write(command)
1✔
201

202
        # Drop a previous command.
203
        command_log.seek(0)
1✔
204
        command_log.truncate()
1✔
205

206
        if not command_exe:
1✔
207
            return
1✔
208

209
        # Send a command to tarantool console.
210
        result = self.send_command(command_exe.getvalue(), ts)
1✔
211

212
        # Convert and prettify a command result.
213
        result = result.replace('\r\n', '\n')
1✔
214
        if self.result_file_version >= 2:
1!
215
            result = prefix_each_line(' | ', result)
1✔
216

217
        # Write a result of the command to a result file.
218
        sys.stdout.write(result)
1✔
219

220
        # Drop a previous command.
221
        command_exe.seek(0)
1✔
222
        command_exe.truncate()
1✔
223

224
    def exec_loop(self, ts):
1✔
225
        self.write_result_file_version_line()
1✔
226
        if not self.execute_pragma_sql_default_engine(ts):
1!
227
            return
×
228

229
        # Set default language for the test.
230
        self.set_language(ts, self.default_language)
1✔
231

232
        # Use two buffers: one to commands that are logged in a
233
        # result file and another that contains commands that
234
        # actually executed on a tarantool console.
235
        command_log = StringIO()
1✔
236
        command_exe = StringIO()
1✔
237

238
        # A newline from a source that is not end of a command is
239
        # replaced with the following symbols.
240
        newline_log = '\n'
1✔
241
        newline_exe = ' '
1✔
242

243
        # A backslash from a source is replaced with the following
244
        # symbols.
245
        backslash_log = '\\'
1✔
246
        backslash_exe = ''
1✔
247

248
        # A newline that marks end of a command is replaced with
249
        # the following symbols.
250
        eoc_log = '\n'
1✔
251
        eoc_exe = '\n'
1✔
252

253
        for line in open(self.name, 'r'):
1✔
254
            # Normalize a line.
255
            line = line.rstrip('\n')
1✔
256

257
            # Skip metainformation (only tags at the moment).
258
            #
259
            # It is to reduce noise changes in result files, when
260
            # tags are added or edited.
261
            #
262
            # TODO: Ideally we should do that only on a first
263
            # comment in the file.
264
            if self.TAGS_LINE_RE.match(line):
1!
265
                continue
×
266

267
            # Show empty lines / comments in a result file, but
268
            # don't send them to tarantool.
269
            line_is_empty = line.strip() == ''
1✔
270
            if line_is_empty or line.find('--') == 0:
1✔
271
                if self.result_file_version >= 2:
1!
272
                    command_log.write(line + eoc_log)
1✔
273
                    self.flush(ts, command_log, None)
1✔
274
                elif line_is_empty:
×
275
                    # Compatibility mode: don't add empty lines to
276
                    # a result file in except when a delimiter is
277
                    # set.
278
                    if command_log.getvalue():
×
279
                        command_log.write(eoc_log)
×
280
                else:
281
                    # Compatibility mode: write a comment and only
282
                    # then a command before it when a delimiter is
283
                    # set.
284
                    sys.stdout.write(line + eoc_log)
×
285
                self.inspector.sem.wait()
1✔
286
                continue
1✔
287

288
            # A delimiter is set and found at end of the line:
289
            # send the command.
290
            if ts.delimiter and line.endswith(ts.delimiter):
1✔
291
                delimiter_len = len(ts.delimiter)
1✔
292
                command_log.write(line + eoc_log)
1✔
293
                command_exe.write(line[:-delimiter_len] + eoc_exe)
1✔
294
                self.flush(ts, command_log, command_exe)
1✔
295
                self.inspector.sem.wait()
1✔
296
                continue
1✔
297

298
            # A backslash found at end of the line: continue
299
            # collecting input. Send / log a backslash as is when
300
            # it is inside a block with set delimiter.
301
            if line.endswith('\\') and not ts.delimiter:
1✔
302
                command_log.write(line[:-1] + backslash_log + newline_log)
1✔
303
                command_exe.write(line[:-1] + backslash_exe + newline_exe)
1✔
304
                self.inspector.sem.wait()
1✔
305
                continue
1✔
306

307
            # A delimiter is set, but not found at the end of the
308
            # line: continue collecting input.
309
            if ts.delimiter:
1✔
310
                command_log.write(line + newline_log)
1✔
311
                command_exe.write(line + newline_exe)
1✔
312
                self.inspector.sem.wait()
1✔
313
                continue
1✔
314

315
            # A delimiter is not set, backslash is not found at
316
            # end of the line: send the command.
317
            command_log.write(line + eoc_log)
1✔
318
            command_exe.write(line + eoc_exe)
1✔
319
            self.flush(ts, command_log, command_exe)
1✔
320
            self.inspector.sem.wait()
1✔
321

322
        # Free StringIO() buffers.
323
        command_log.close()
1✔
324
        command_exe.close()
1✔
325

326
    def execute(self, server):
1✔
327
        super(LuaTest, self).execute(server)
1✔
328

329
        # Track the same process metrics as part of another test.
330
        sampler.register_process(server.process.pid, self.id, server.name)
1✔
331

332
        cls_name = server.__class__.__name__.lower()
1✔
333
        if 'gdb' in cls_name or 'lldb' in cls_name or 'strace' in cls_name:
1!
334
            # don't propagate gdb/lldb/strace mixin to non-default servers,
335
            # it doesn't work properly for now
336
            # TODO: strace isn't interactive, so it's easy to make it works for
337
            #       non-default server
338
            create_server = TarantoolServer
×
339
        else:
340
            # propagate valgrind mixin to non-default servers
341
            create_server = server.__class__
1✔
342
        ts = TestState(
1✔
343
            self.suite_ini, server, create_server,
344
            self.run_params
345
        )
346
        self.inspector.set_parser(ts)
1✔
347
        lua = TestRunGreenlet(self.exec_loop, ts)
1✔
348
        self.current_test_greenlet = lua
1✔
349
        lua.start()
1✔
350
        try:
1✔
351
            save_join(lua, timeout=Options().args.test_timeout)
1✔
352
        except KeyboardInterrupt:
×
353
            # prevent tests greenlet from writing to the real stdout
354
            lua.kill()
×
355
            raise
×
356
        except TarantoolStartError as e:
×
357
            color_stdout('\n[Instance "{0}"] Failed to start tarantool '
×
358
                         'instance "{1}"\n'.format(server.name, e.name),
359
                         schema='error')
360
            server.kill_current_test()
×
361
        finally:
362
            # Stop any servers created by the test, except the
363
            # default one.
364
            #
365
            # The stop_nondefault() method calls
366
            # TarantoolServer.stop() under the hood. It sends
367
            # SIGTERM (if another signal is not passed), waits
368
            # for 5 seconds for a process termination and, if
369
            # nothing occurs, sends SIGKILL and continue waiting
370
            # for the termination.
371
            #
372
            # Look, 5 seconds (plus some delay for waiting) for
373
            # each instance if it does not follow SIGTERM[^1].
374
            # It is unacceptable, because the difference between
375
            # --test-timeout (110 seconds by default) and
376
            # --no-output-timeout (120 seconds by default) may
377
            # be lower than (5 seconds + delay) * (non-default
378
            # instance count).
379
            #
380
            # That's why we send SIGKILL for residual instances
381
            # right away.
382
            #
383
            # Hitting --no-output-timeout is undesirable, because
384
            # in the current implementation it is the show-stopper
385
            # for a testing: test-run doesn't restart fragile
386
            # tests, doesn't continue processing of other tests,
387
            # doesn't save artifacts at the end of the testing.
388
            #
389
            # [^1]: See gh-4127 and gh-5573 for problems of this
390
            #       kind.
391
            ts.stop_nondefault(signal=signal.SIGKILL)
1✔
392

393

394
class PythonTest(Test):
1✔
395
    """ Handle *.test.py test files. """
396

397
    def execute(self, server):
1✔
398
        super(PythonTest, self).execute(server)
1✔
399

400
        # Track the same process metrics as part of another test.
401
        sampler.register_process(server.process.pid, self.id, server.name)
1✔
402

403
        new_globals = dict(locals(), test_run_current_test=self, **server.__dict__)
1✔
404
        with open(self.name) as f:
1✔
405
            code = compile(f.read(), self.name, 'exec')
1✔
406

407
        try:
1✔
408
            exec(code, new_globals)
1✔
409
        except TarantoolStartError:
×
410
            # fail tests in the case of catching server start errors
411
            raise TestExecutionError
×
412

413
        # crash was detected (possibly on non-default server)
414
        if server.current_test.is_crash_reported:
1!
415
            raise TestExecutionError
×
416

417

418
CON_SWITCH = {
1✔
419
    'lua': AdminAsyncConnection,
420
    'python': AdminConnection
421
}
422

423

424
class TarantoolStartError(OSError):
1✔
425
    def __init__(self, name=None, timeout=None, reason=None):
1✔
426
        self.name = name
×
427
        self.timeout = timeout
×
428
        self.reason = reason
×
429

430
    def __str__(self):
1✔
431
        message = '[Instance "{}"] Failed to start'.format(self.name)
×
432
        if self.timeout:
×
433
            message = "{} within {} seconds".format(message, self.timeout)
×
434
        if self.reason:
×
435
            message = "{}: {}".format(message, self.reason)
×
436
        return "\n{}\n".format(message)
×
437

438

439
class TarantoolLog(object):
1✔
440
    def __init__(self, path):
1✔
441
        self.path = path
1✔
442
        self.log_begin = 0
1✔
443

444
    def open(self, mode, **kwargs):
1✔
445
        if PY3:
1!
446
            # Sometimes the server's log file may contain bytes that cannot be
447
            # decoded by utf-8 codec and test-run fails with an error like this:
448
            #     UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf0 in
449
            #         position 660: invalid continuation byte
450
            # The option below fixes it. Note, Python2 doesn't know the option.
451
            kwargs['errors'] = 'replace'
1✔
452
        return open(self.path, mode, **kwargs)
1✔
453

454
    def positioning(self):
1✔
455
        if os.path.exists(self.path):
1✔
456
            with self.open('r') as f:
1✔
457
                f.seek(0, os.SEEK_END)
1✔
458
                self.log_begin = f.tell()
1✔
459
        return self
1✔
460

461
    def seek_once(self, msg):
1✔
462
        if not os.path.exists(self.path):
×
463
            return -1
×
464
        with self.open('r') as f:
×
465
            f.seek(self.log_begin, os.SEEK_SET)
×
466
            while True:
467
                log_str = f.readline()
×
468

469
                if not log_str:
×
470
                    return -1
×
471
                pos = log_str.find(msg)
×
472
                if pos != -1:
×
473
                    return pos
×
474

475
    def seek_wait(self, msg, proc=None, name=None, deadline=None):
1✔
476
        timeout = Options().args.server_start_timeout
1✔
477
        while not deadline or time.time() < deadline:
1!
478
            if os.path.exists(self.path):
1!
479
                break
1✔
480
            gevent.sleep(0.001)
×
481
        else:
482
            color_stdout("\nFailed to locate {} logfile within {} "
×
483
                         "seconds\n".format(self.path, timeout), schema='error')
484
            return False
×
485

486
        with self.open('r') as f:
1✔
487
            f.seek(self.log_begin, os.SEEK_SET)
1✔
488
            cur_pos = self.log_begin
1✔
489
            while not deadline or time.time() < deadline:
1!
490
                if not (proc is None):
1!
491
                    # proc.poll() returns None if the process is working. When
492
                    # the process completed, it returns the process exit code.
493
                    if not (proc.poll() is None):
1!
494
                        # Raise an error since the process completed.
495
                        raise TarantoolStartError(name)
×
496
                log_str = f.readline()
1✔
497
                if not log_str:
1✔
498
                    # We reached the end of the logfile.
499
                    gevent.sleep(0.001)
1✔
500
                    f.seek(cur_pos, os.SEEK_SET)
1✔
501
                    continue
1✔
502
                else:
503
                    # if the whole line is read, check the pattern in the line.
504
                    # Otherwise, set the cursor back to read the line again.
505
                    if log_str.endswith('\n'):
1!
506
                        if re.findall(msg, log_str):
1✔
507
                            return True
1✔
508
                    else:
509
                        gevent.sleep(0.001)
×
510
                        f.seek(cur_pos, os.SEEK_SET)
×
511
                        continue
×
512
                cur_pos = f.tell()
1✔
513

514
        color_stdout("\nFailed to find '{}' pattern in {} logfile within {} "
×
515
                     "seconds\n".format(msg, self.path, timeout),
516
                     schema='error')
517

518
        return False
×
519

520

521
class TarantoolServer(Server):
1✔
522
    default_tarantool = {
1✔
523
        "bin": "tarantool",
524
        "logfile": "tarantool.log",
525
        "pidfile": "tarantool.pid",
526
        "name": "default",
527
        "ctl": "tarantoolctl",
528
    }
529

530
    # ----------------------------PROPERTIES--------------------------------- #
531
    @property
1✔
532
    def name(self):
1✔
533
        if not hasattr(self, '_name') or not self._name:
1!
534
            return self.default_tarantool["name"]
×
535
        return self._name
1✔
536

537
    @name.setter
1✔
538
    def name(self, val):
1✔
539
        self._name = val
1✔
540

541
    @property
1✔
542
    def logfile(self):
1✔
543
        if not hasattr(self, '_logfile') or not self._logfile:
1!
544
            return os.path.join(self.vardir, self.default_tarantool["logfile"])
×
545
        return self._logfile
1✔
546

547
    @logfile.setter
1✔
548
    def logfile(self, val):
1✔
549
        self._logfile = os.path.join(self.vardir, val)
1✔
550

551
    @property
1✔
552
    def pidfile(self):
1✔
553
        if not hasattr(self, '_pidfile') or not self._pidfile:
1!
554
            return os.path.join(self.vardir, self.default_tarantool["pidfile"])
1✔
555
        return self._pidfile
×
556

557
    @pidfile.setter
1✔
558
    def pidfile(self, val):
1✔
559
        self._pidfile = os.path.join(self.vardir, val)
1✔
560

561
    @property
1✔
562
    def builddir(self):
1✔
563
        if not hasattr(self, '_builddir'):
×
564
            raise ValueError("No build-dir is specified")
×
565
        return self._builddir
×
566

567
    @builddir.setter
1✔
568
    def builddir(self, val):
1✔
569
        if val is None:
×
570
            return
×
571
        self._builddir = os.path.abspath(val)
×
572

573
    @property
1✔
574
    def script_dst(self):
1✔
575
        return os.path.join(self.vardir, os.path.basename(self.script))
1✔
576

577
    @property
1✔
578
    def logfile_pos(self):
1✔
579
        if not hasattr(self, '_logfile_pos'):
1!
580
            self._logfile_pos = None
×
581
        return self._logfile_pos
1✔
582

583
    @logfile_pos.setter
1✔
584
    def logfile_pos(self, val):
1✔
585
        self._logfile_pos = TarantoolLog(val).positioning()
1✔
586

587
    @property
1✔
588
    def script(self):
1✔
589
        if not hasattr(self, '_script'):
1!
590
            self._script = None
×
591
        return self._script
1✔
592

593
    @script.setter
1✔
594
    def script(self, val):
1✔
595
        if val is None:
1✔
596
            if hasattr(self, '_script'):
1!
597
                delattr(self, '_script')
×
598
            return
1✔
599
        self._script = os.path.abspath(val)
1✔
600
        self.name = os.path.basename(self._script).rsplit(".", maxsplit=1)[0]
1✔
601

602
    @property
1✔
603
    def _admin(self):
1✔
604
        if not hasattr(self, 'admin'):
1!
605
            self.admin = None
×
606
        return self.admin
1✔
607

608
    @_admin.setter
1✔
609
    def _admin(self, port):
1✔
610
        if hasattr(self, 'admin'):
1!
611
            del self.admin
×
612
        if not hasattr(self, 'tests_type'):
1✔
613
            self.tests_type = 'lua'
1✔
614
        self.admin = CON_SWITCH[self.tests_type](self.localhost, port)
1✔
615

616
    @property
1✔
617
    def _iproto(self):
1✔
618
        if not hasattr(self, 'iproto'):
×
619
            self.iproto = None
×
620
        return self.iproto
×
621

622
    @_iproto.setter
1✔
623
    def _iproto(self, port):
1✔
624
        if hasattr(self, 'iproto'):
1!
625
            del self.iproto
×
626
        self.iproto = BoxConnection(self.localhost, port)
1✔
627

628
    @property
1✔
629
    def log_des(self):
1✔
630
        if not hasattr(self, '_log_des'):
1✔
631
            self._log_des = open(self.logfile, 'ab')
1✔
632
        return self._log_des
1✔
633

634
    @log_des.deleter
1✔
635
    def log_des(self):
1✔
636
        if not hasattr(self, '_log_des'):
1!
637
            return
×
638
        if not self._log_des.closed:
1!
639
            self._log_des.close()
1✔
640
        delattr(self, '_log_des')
1✔
641

642
    @property
1✔
643
    def rpl_master(self):
1✔
644
        if not hasattr(self, '_rpl_master'):
1✔
645
            self._rpl_master = None
1✔
646
        return self._rpl_master
1✔
647

648
    @rpl_master.setter
1✔
649
    def rpl_master(self, val):
1✔
650
        if not isinstance(self, (TarantoolServer, None)):
1!
651
            raise ValueError('Replication master must be Tarantool'
×
652
                             ' Server class, his derivation or None')
653
        self._rpl_master = val
1✔
654

655
    # ----------------------------------------------------------------------- #
656

657
    def __new__(cls, ini=None, *args, **kwargs):
1✔
658
        cls = Server.get_mixed_class(cls, ini)
1✔
659
        return object.__new__(cls)
1✔
660

661
    def __init__(self, _ini=None, test_suite=None):
1✔
662
        if _ini is None:
1✔
663
            _ini = {}
1✔
664
        ini = {
1✔
665
            'core': 'tarantool',
666
            'gdb': False,
667
            'lldb': False,
668
            'script': None,
669
            'lua_libs': [],
670
            'valgrind': False,
671
            'vardir': None,
672
            'use_unix_sockets_iproto': False,
673
            'tarantool_port': None,
674
            'strace': False
675
        }
676
        ini.update(_ini)
1✔
677
        if ini.get('use_unix_sockets') is not None:
1!
678
            qa_notice(textwrap.fill('The "use_unix_sockets" option is defined '
×
679
                                    'in suite.ini, but it was dropped in favor '
680
                                    'of permanent using Unix sockets'))
681
        Server.__init__(self, ini, test_suite)
1✔
682
        self.testdir = os.path.abspath(os.curdir)
1✔
683
        self.sourcedir = os.path.abspath(os.path.join(os.path.basename(
1✔
684
            sys.argv[0]), "..", ".."))
685
        self.name = "default"
1✔
686
        self.conf = {}
1✔
687
        self.status = None
1✔
688
        self.core = ini['core']
1✔
689
        self.localhost = '127.0.0.1'
1✔
690
        self.gdb = ini['gdb']
1✔
691
        self.lldb = ini['lldb']
1✔
692
        self.script = ini['script']
1✔
693
        self.lua_libs = ini['lua_libs']
1✔
694
        self.valgrind = ini['valgrind']
1✔
695
        self.strace = ini['strace']
1✔
696
        self.use_unix_sockets_iproto = ini['use_unix_sockets_iproto']
1✔
697
        self._start_against_running = ini['tarantool_port']
1✔
698
        self.crash_detector = None
1✔
699
        # use this option with inspector to enable crashes in test
700
        self.crash_enabled = False
1✔
701
        # set in from a test let test-run ignore server's crashes
702
        self.crash_expected = False
1✔
703
        # filled in {Test,FuncTest,LuaTest,PythonTest}.execute()
704
        # or passed through execfile() for PythonTest
705
        self.current_test = None
1✔
706
        caller_globals = inspect.stack()[1][0].f_globals
1✔
707
        if 'test_run_current_test' in caller_globals.keys():
1!
708
            self.current_test = caller_globals['test_run_current_test']
×
709

710
    @classmethod
1✔
711
    def find_exe(cls, builddir, silent=True, executable=None):
1✔
712
        cls.builddir = os.path.abspath(builddir)
1✔
713
        builddir = os.path.join(builddir, "src")
1✔
714
        path = builddir + os.pathsep + os.environ["PATH"]
1✔
715
        color_log("Looking for server binary in ", schema='serv_text')
1✔
716
        color_log(path + ' ...\n', schema='path')
1✔
717
        for _dir in path.split(os.pathsep):
1!
718
            exe = executable or os.path.join(_dir, cls.default_tarantool["bin"])
1✔
719
            ctl_dir = cls.TEST_RUN_DIR
1✔
720
            ctl = os.path.join(ctl_dir, cls.default_tarantool['ctl'])
1✔
721
            need_lua_path = False
1✔
722
            if os.path.isdir(ctl) or not os.access(ctl, os.X_OK):
1!
723
                ctl_dir = os.path.join(_dir, '../extra/dist')
×
724
                ctl = os.path.join(ctl_dir, cls.default_tarantool['ctl'])
×
725
                need_lua_path = True
×
726
            if os.access(exe, os.X_OK) and os.access(ctl, os.X_OK):
1✔
727
                cls.binary = os.path.abspath(exe)
1✔
728
                cls.ctl_path = os.path.abspath(ctl)
1✔
729
                cls.ctl_plugins = os.path.abspath(
1✔
730
                    os.path.join(ctl_dir, '..')
731
                )
732
                prepend_path(ctl_dir)
1✔
733
                prepend_path(_dir)
1✔
734
                os.environ["TARANTOOLCTL"] = ctl
1✔
735
                if need_lua_path:
1!
736
                    os.environ["LUA_PATH"] = \
×
737
                        ctl_dir + '/?.lua;' + \
738
                        ctl_dir + '/?/init.lua;' + \
739
                        os.environ.get("LUA_PATH", ";;")
740
                cls.debug = bool(re.findall(r'^Target:.*-Debug$', str(cls.version()),
1✔
741
                                            re.M))
742
                return exe
1✔
743
        raise RuntimeError("Can't find server executable in " + path)
×
744

745
    @classmethod
1✔
746
    def print_exe(cls):
1✔
747
        color_stdout('Tarantool server information:\n', schema='info')
1✔
748
        if cls.binary:
1!
749
            color_stdout(' | Found executable at {}\n'.format(cls.binary))
1✔
750
        if cls.ctl_path:
1!
751
            color_stdout(' | Found tarantoolctl at {}\n'.format(cls.ctl_path))
1✔
752
        color_stdout('\n' + prefix_each_line(' | ', cls.version()) + '\n',
1✔
753
                     schema='version')
754
        color_stdout('Detected build mode: {}\n'.format(
1✔
755
                     'Debug' if cls.debug else 'Release'), schema='info')
756

757
    def install(self, silent=True):
1✔
758
        if self._start_against_running:
1!
759
            self._iproto = self._start_against_running
×
760
            self._admin = int(self._start_against_running) + 1
×
761
            return
×
762
        color_log('DEBUG: [Instance {}] Installing the server...\n'.format(
1✔
763
            self.name), schema='info')
764
        color_log(' | Found executable at {}\n'.format(self.binary))
1✔
765
        color_log(' | Found tarantoolctl at {}\n'.format(self.ctl_path))
1✔
766
        color_log(' | Creating and populating working directory in '
1✔
767
                  '{}...\n'.format(self.vardir))
768
        if not os.path.exists(self.vardir):
1✔
769
            os.makedirs(self.vardir)
1✔
770
        else:
771
            color_log(' | Found old workdir, deleting...\n')
1✔
772
            self.kill_old_server()
1✔
773
            self.cleanup()
1✔
774
        self.copy_files()
1✔
775

776
        path = os.path.join(self.vardir, self.name + ".c")
1✔
777
        warn_unix_socket(path)
1✔
778
        self._admin = path
1✔
779

780
        if self.use_unix_sockets_iproto:
1!
781
            path = os.path.join(self.vardir, self.name + ".i")
×
782
            warn_unix_socket(path)
×
783
            self.listen_uri = path
×
784
            self._iproto = path
×
785
        else:
786
            self.listen_uri = self.localhost + ':0'
1✔
787

788
        # these sockets will be created by tarantool itself
789
        path = os.path.join(self.vardir, self.name + '.control')
1✔
790
        warn_unix_socket(path)
1✔
791

792
    def deploy(self, silent=True, **kwargs):
1✔
793
        self.install(silent)
1✔
794
        self.start(silent=silent, **kwargs)
1✔
795

796
    def copy_files(self):
1✔
797
        if self.script:
1!
798
            shutil.copy(self.script, self.script_dst)
1✔
799
            os.chmod(self.script_dst, 0o777)
1✔
800
        if self.lua_libs:
1✔
801
            for i in self.lua_libs:
1!
802
                source = os.path.join(self.testdir, i)
×
803
                try:
×
804
                    if os.path.isdir(source):
×
805
                        shutil.copytree(source,
×
806
                                        os.path.join(self.vardir,
807
                                                     os.path.basename(source)))
808
                    else:
809
                        shutil.copy(source, self.vardir)
×
810
                except IOError as e:
×
811
                    if (e.errno == errno.ENOENT):
×
812
                        continue
×
813
                    raise
×
814
        # Previously tarantoolctl configuration file located in tarantool
815
        # repository at test/ directory. Currently it is located in root
816
        # path of test-run/ submodule repository. For backward compatibility
817
        # this file should be checked at the old place and only after at
818
        # the current.
819
        tntctl_file = '.tarantoolctl'
1✔
820
        if not os.path.exists(tntctl_file):
1!
821
            tntctl_file = os.path.join(self.TEST_RUN_DIR, '.tarantoolctl')
1✔
822
        shutil.copy(tntctl_file, self.vardir)
1✔
823
        shutil.copy(os.path.join(self.TEST_RUN_DIR, 'test_run.lua'),
1✔
824
                    self.vardir)
825

826
        if self.snapshot_path:
1!
827
            # Copy snapshot to the workdir.
828
            # Usually Tarantool looking for snapshots on start in a current directory
829
            # or in a directories that specified in memtx_dir or vinyl_dir box settings.
830
            # Before running test current directory (workdir) passed to a new instance in
831
            # an environment variable TEST_WORKDIR and then tarantoolctl
832
            # adds to it instance_name and set to memtx_dir and vinyl_dir.
833
            (instance_name, _) = os.path.splitext(os.path.basename(self.script))
×
834
            instance_dir = os.path.join(self.vardir, instance_name)
×
835
            safe_makedirs(instance_dir)
×
836
            snapshot_dest = os.path.join(instance_dir, DEFAULT_SNAPSHOT_NAME)
×
837
            color_log("Copying snapshot {} to {}\n".format(
×
838
                self.snapshot_path, snapshot_dest))
839
            shutil.copy(self.snapshot_path, snapshot_dest)
×
840

841
    def prepare_args(self, args=[]):
1✔
842
        cli_args = [self.ctl_path, 'start',
1✔
843
                    os.path.basename(self.script)] + args
844
        if self.disable_schema_upgrade:
1!
845
            cli_args = [self.binary, '-e',
×
846
                        self.DISABLE_AUTO_UPGRADE] + cli_args
847

848
        return cli_args
1✔
849

850
    def pretest_clean(self):
1✔
851
        # Tarantool servers restarts before each test on the worker.
852
        # Snap and logs are removed within it.
853
        pass
1✔
854

855
    def cleanup(self, *args, **kwargs):
1✔
856
        # For `core = tarantool` tests default worker runs on
857
        # subdirectory created by tarantoolctl by using script name
858
        # from suite.ini file.
859
        super(TarantoolServer, self).cleanup(dirname=self.name,
1✔
860
                                             *args, **kwargs)
861

862
    def start(self, silent=True, wait=True, wait_load=True, rais=True, args=[],
1✔
863
              **kwargs):
864
        if self._start_against_running:
1!
865
            return
×
866
        if self.status == 'started':
1!
867
            if not silent:
×
868
                color_stdout('The server is already started.\n',
×
869
                             schema='lerror')
870
            return
×
871

872
        args = self.prepare_args(args)
1✔
873
        self.pidfile = '%s.pid' % self.name
1✔
874
        self.logfile = '%s.log' % self.name
1✔
875

876
        path = self.script_dst if self.script else \
1✔
877
            os.path.basename(self.binary)
878
        color_log('DEBUG: [Instance {}] Starting the server...\n'.format(
1✔
879
            self.name), schema='info')
880
        color_log(' | ' + path + '\n', schema='path')
1✔
881
        color_log(prefix_each_line(' | ', self.version()) + '\n',
1✔
882
                  schema='version')
883

884
        os.putenv("LISTEN", self.listen_uri)
1✔
885
        os.putenv("ADMIN", self.admin.uri)
1✔
886
        if self.rpl_master:
1✔
887
            os.putenv("MASTER", self.rpl_master.iproto.uri)
1✔
888
        self.logfile_pos = self.logfile
1✔
889

890
        # This is strange, but tarantooctl leans on the PWD
891
        # environment variable, not a real current working
892
        # directory, when it performs search for the
893
        # .tarantoolctl configuration file.
894
        os.environ['PWD'] = self.vardir
1✔
895

896
        # redirect stdout from tarantoolctl and tarantool
897
        os.putenv("TEST_WORKDIR", self.vardir)
1✔
898
        self.process = subprocess.Popen(args,
1✔
899
                                        cwd=self.vardir,
900
                                        stdout=self.log_des,
901
                                        stderr=self.log_des)
902
        del self.log_des
1✔
903

904
        # Restore the actual PWD value.
905
        os.environ['PWD'] = os.getcwd()
1✔
906

907
        # Track non-default server metrics as part of current
908
        # test.
909
        if self.current_test:
1✔
910
            sampler.register_process(self.process.pid, self.current_test.id,
1✔
911
                                     self.name)
912

913
        # gh-19 crash detection
914
        self.crash_detector = TestRunGreenlet(self.crash_detect)
1✔
915
        self.crash_detector.info = "Crash detector: %s" % self.process
1✔
916
        self.crash_detector.start()
1✔
917

918
        if wait:
1!
919
            deadline = time.time() + Options().args.server_start_timeout
1✔
920
            try:
1✔
921
                self.wait_until_started(wait_load, deadline)
1✔
922
            except TarantoolStartError as err:
×
923
                # Python tests expect we raise an exception when non-default
924
                # server fails
925
                if self.crash_expected:
×
926
                    raise
×
927
                if not (self.current_test and
×
928
                        self.current_test.is_crash_reported):
929
                    if self.current_test:
×
930
                        self.current_test.is_crash_reported = True
×
931
                    color_stdout(err, schema='error')
×
932
                    self.print_log(15)
×
933
                # Raise exception when caller ask for it (e.g. in case of
934
                # non-default servers)
935
                if rais:
×
936
                    raise
×
937
                # if the server fails before any test started, we should inform
938
                # a caller by the exception
939
                if not self.current_test:
×
940
                    raise
×
941
                self.kill_current_test()
×
942

943
        port = self.admin.port
1✔
944
        self.admin.disconnect()
1✔
945
        self.admin = CON_SWITCH[self.tests_type](self.localhost, port)
1✔
946

947
        if not self.use_unix_sockets_iproto:
1!
948
            if wait and wait_load and not self.crash_expected:
1!
949
                self._iproto = self.get_iproto_port()
1✔
950

951
        self.status = 'started'
1✔
952

953
        # Verify that the schema actually was not upgraded.
954
        if self.disable_schema_upgrade:
1!
955
            expected_version = extract_schema_from_snapshot(self.snapshot_path)
×
956
            actual_version = tuple(yaml.safe_load(self.admin.execute(
×
957
                'box.space._schema:get{"version"}'))[0][1:])
958
            if expected_version != actual_version:
×
959
                color_stdout('Schema version check fails: expected '
×
960
                             '{}, got {}\n'.format(expected_version,
961
                                                   actual_version),
962
                             schema='error')
963
                raise TarantoolStartError(self.name)
×
964

965
    def crash_detect(self):
1✔
966
        if self.crash_expected:
1!
967
            return
×
968

969
        while self.process.returncode is None:
1✔
970
            self.process.poll()
1✔
971
            if self.process.returncode is None:
1✔
972
                gevent.sleep(0.1)
1✔
973

974
        if self.process.returncode in [0, -signal.SIGABRT, -signal.SIGKILL, -signal.SIGTERM]:
1!
975
            return
1✔
976

977
        self.kill_current_test()
×
978

979
        if not os.path.exists(self.logfile):
×
980
            return
×
981

982
        if not self.current_test.is_crash_reported:
×
983
            self.current_test.is_crash_reported = True
×
984
            self.crash_grep()
×
985

986
    def crash_grep(self):
1✔
987
        print_log_lines = 15
×
988
        assert_fail_re = re.compile(r'^.*: Assertion .* failed\.$')
×
989

990
        # find and save backtrace or assertion fail
991
        assert_lines = list()
×
992
        bt = list()
×
993
        with self.logfile_pos.open('r') as log:
×
994
            lines = log.readlines()
×
995
            for rpos, line in enumerate(reversed(lines)):
×
996
                if line.startswith('Segmentation fault'):
×
997
                    bt = lines[-rpos - 1:]
×
998
                    break
×
999
                if assert_fail_re.match(line):
×
1000
                    pos = len(lines) - rpos
×
1001
                    assert_lines = lines[max(0, pos - print_log_lines):pos]
×
1002
                    break
×
1003
            else:
1004
                bt = list()
×
1005

1006
        # print insident meat
1007
        if self.process.returncode < 0:
×
1008
            color_stdout('\n\n[Instance "%s" killed by signal: %d (%s)]\n' % (
×
1009
                self.name, -self.process.returncode,
1010
                signame(-self.process.returncode)), schema='error')
1011
        else:
1012
            color_stdout('\n\n[Instance "%s" returns with non-zero exit code: '
×
1013
                         '%d]\n' % (self.name, self.process.returncode),
1014
                         schema='error')
1015

1016
        # print assert line if any and return
1017
        if assert_lines:
×
1018
            color_stdout('Found assertion fail in the results file '
×
1019
                         '[%s]:\n' % self.logfile,
1020
                         schema='error')
1021
            sys.stderr.flush()
×
1022
            for line in assert_lines:
×
1023
                sys.stderr.write(line)
×
1024
            sys.stderr.flush()
×
1025
            return
×
1026

1027
        # print backtrace if any
1028
        sys.stderr.flush()
×
1029
        for trace in bt:
×
1030
            sys.stderr.write(trace)
×
1031

1032
        # print log otherwise (if backtrace was not found)
1033
        if not bt:
×
1034
            self.print_log(print_log_lines)
×
1035
        sys.stderr.flush()
×
1036

1037
    def kill_current_test(self):
1✔
1038
        """ Unblock save_join() call inside LuaTest.execute(), which doing
1039
            necessary servers/greenlets clean up.
1040
        """
1041
        # current_test_greenlet is None for PythonTest
1042
        if self.current_test.current_test_greenlet:
×
1043
            gevent.kill(self.current_test.current_test_greenlet)
×
1044

1045
    def wait_stop(self):
1✔
1046
        self.process.wait()
1✔
1047

1048
    def stop(self, silent=True, signal=signal.SIGTERM):
1✔
1049
        """ Kill tarantool server using specified signal (SIGTERM by default)
1050

1051
            signal - a number of a signal
1052
        """
1053
        if self._start_against_running:
1!
1054
            color_log('Server [%s] start against running ...\n',
×
1055
                      schema='test_var')
1056
            return
×
1057
        if self.status != 'started' and not hasattr(self, 'process'):
1!
1058
            if not silent:
×
1059
                raise Exception('Server is not started')
×
1060
            else:
1061
                color_log(
×
1062
                    'Server [{0.name}] is not started '
1063
                    '(status:{0.status}) ...\n'.format(self),
1064
                    schema='test_var'
1065
                )
1066
            return
×
1067
        if not silent:
1!
1068
            color_stdout('[Instance {}] Stopping the server...\n'.format(
×
1069
                self.name), schema='info')
1070
        else:
1071
            color_log('DEBUG: [Instance {}] Stopping the server...\n'.format(
1✔
1072
                self.name), schema='info')
1073
        # kill only if process is alive
1074
        if self.process is not None and self.process.returncode is None:
1!
1075
            color_log(' | Sending signal {0} ({1}) to {2}\n'.format(
1✔
1076
                      signal, signame(signal),
1077
                      format_process(self.process.pid)))
1078
            try:
1✔
1079
                self.process.send_signal(signal)
1✔
1080
            except OSError:
×
1081
                pass
×
1082

1083
            # Waiting for stopping the server. If the timeout
1084
            # reached, send SIGKILL.
1085
            timeout = 5
1✔
1086

1087
            def kill():
1✔
1088
                qa_notice('The server \'{}\' does not stop during {} '
×
1089
                          'seconds after the {} ({}) signal.\n'
1090
                          'Info: {}\n'
1091
                          'Sending SIGKILL...'.format(
1092
                              self.name, timeout, signal, signame(signal),
1093
                              format_process(self.process.pid)))
1094
                try:
×
1095
                    self.process.kill()
×
1096
                except OSError:
×
1097
                    pass
×
1098

1099
            timer = Timer(timeout, kill)
1✔
1100
            timer.start()
1✔
1101
            if self.crash_detector is not None:
1!
1102
                save_join(self.crash_detector)
1✔
1103
            self.wait_stop()
1✔
1104
            timer.cancel()
1✔
1105

1106
        self.status = None
1✔
1107
        if re.search(r'^/', str(self._admin.port)):
1!
1108
            if os.path.exists(self._admin.port):
1!
1109
                os.unlink(self._admin.port)
1✔
1110

1111
    def restart(self):
1✔
1112
        self.stop()
×
1113
        self.start()
×
1114

1115
    def kill_old_server(self, silent=True):
1✔
1116
        pid = self.read_pidfile()
1✔
1117
        if pid == -1:
1!
1118
            return False
1✔
1119
        if not silent:
×
1120
            color_stdout(
×
1121
                '    Found old server, pid {0}, killing ...'.format(pid),
1122
                schema='info'
1123
            )
1124
        else:
1125
            color_log('    Found old server, pid {0}, killing ...'.format(pid),
×
1126
                      schema='info')
1127
        try:
×
1128
            os.kill(pid, signal.SIGTERM)
×
1129
        except OSError:
×
1130
            pass
×
1131
        self.wait_until_stopped(pid)
×
1132
        return True
×
1133

1134
    def wait_load(self, deadline):
1✔
1135
        """Wait until the server log file is matched the entry pattern
1136

1137
        If the entry pattern couldn't be found in a log file until a timeout
1138
        is up, it will raise a TarantoolStartError exception.
1139
        """
1140
        msg = 'entering the event loop|will retry binding|hot standby mode'
1✔
1141
        p = self.process if not self.gdb and not self.lldb else None
1✔
1142
        if not self.logfile_pos.seek_wait(msg, p, self.name, deadline):
1!
1143
            raise TarantoolStartError(
×
1144
                self.name, Options().args.server_start_timeout)
1145

1146
    def wait_until_started(self, wait_load=True, deadline=None):
1✔
1147
        """ Wait until server is started.
1148

1149
        Server consists of two parts:
1150
        1) wait until server is listening on sockets
1151
        2) wait until server tells us his status
1152

1153
        """
1154
        color_log('DEBUG: [Instance {}] Waiting until started '
1✔
1155
                  '(wait_load={})\n'.format(self.name, str(wait_load)),
1156
                  schema='info')
1157
        if wait_load:
1!
1158
            self.wait_load(deadline)
1✔
1159
        while not deadline or time.time() < deadline:
1!
1160
            try:
1✔
1161
                temp = AdminConnection(self.localhost, self.admin.port)
1✔
1162
                if not wait_load:
1!
1163
                    ans = yaml.safe_load(temp.execute("2 + 2"))
×
1164
                    color_log(" | Successful connection check; don't wait for "
×
1165
                              "loading")
1166
                    return True
×
1167
                ans = yaml.safe_load(temp.execute('box.info.status'))[0]
1✔
1168
                if ans in ('running', 'hot_standby', 'orphan'):
1!
1169
                    color_log(" | Started {} (box.info.status: '{}')\n".format(
1✔
1170
                        format_process(self.process.pid), ans))
1171
                    return True
1✔
1172
                elif ans in ('loading',):
×
1173
                    continue
×
1174
                else:
1175
                    raise Exception(
×
1176
                        "Strange output for `box.info.status`: %s" % (ans)
1177
                    )
1178
            except socket.error as e:
×
1179
                if e.errno == errno.ECONNREFUSED:
×
1180
                    color_log(' | Connection refused; will retry every 0.1 '
×
1181
                              'seconds...')
1182
                    gevent.sleep(0.1)
×
1183
                    continue
×
1184
                raise
×
1185
            except BrokenConsoleHandshake as e:
×
1186
                raise TarantoolStartError(self.name, reason=e)
×
1187
        else:
1188
            raise TarantoolStartError(
×
1189
                self.name, Options().args.server_start_timeout)
1190

1191
    def wait_until_stopped(self, pid):
1✔
1192
        while True:
1193
            try:
×
1194
                gevent.sleep(0.01)
×
1195
                os.kill(pid, 0)
×
1196
                continue
×
1197
            except OSError:
×
1198
                break
×
1199

1200
    def read_pidfile(self):
1✔
1201
        pid = -1
1✔
1202
        if os.path.exists(self.pidfile):
1!
1203
            try:
×
1204
                with open(self.pidfile) as f:
×
1205
                    pid = int(f.read())
×
1206
            except Exception:
×
1207
                pass
×
1208
        return pid
1✔
1209

1210
    def test_option_get(self, option_list_str, silent=False):
1✔
1211
        args = [self.binary] + shlex.split(option_list_str)
×
1212
        if not silent:
×
1213
            print(" ".join([os.path.basename(self.binary)] + args[1:]))
×
1214
        output = subprocess.Popen(args,
×
1215
                                  cwd=self.vardir,
1216
                                  stdout=subprocess.PIPE,
1217
                                  stderr=subprocess.STDOUT).stdout.read()
1218
        return bytes_to_str(output)
×
1219

1220
    def test_option(self, option_list_str):
1✔
1221
        print(self.test_option_get(option_list_str))
×
1222

1223
    @staticmethod
1✔
1224
    def find_tests(test_suite, suite_path):
1✔
1225
        test_suite.ini['suite'] = suite_path
1✔
1226

1227
        def get_tests(*patterns):
1✔
1228
            res = []
1✔
1229
            for pattern in patterns:
1✔
1230
                path_pattern = os.path.join(suite_path, pattern)
1✔
1231
                res.extend(sorted(glob.glob(path_pattern)))
1✔
1232
            return Server.exclude_tests(res, test_suite.args.exclude)
1✔
1233

1234
        # Add Python tests.
1235
        tests = [PythonTest(k, test_suite.args, test_suite.ini)
1✔
1236
                 for k in get_tests("*.test.py")]
1237

1238
        # Add Lua and SQL tests. One test can appear several times
1239
        # with different configuration names (as configured in a
1240
        # file set by 'config' suite.ini option, usually *.cfg).
1241
        for k in get_tests("*.test.lua", "*.test.sql"):
1✔
1242
            runs = test_suite.get_multirun_params(k)
1✔
1243

1244
            def is_correct(run_name):
1✔
1245
                return test_suite.args.conf is None or \
1✔
1246
                    test_suite.args.conf == run_name
1247

1248
            if runs:
1!
1249
                tests.extend([LuaTest(
1✔
1250
                    k,
1251
                    test_suite.args,
1252
                    test_suite.ini,
1253
                    runs[r],
1254
                    r
1255
                ) for r in runs.keys() if is_correct(r)])
1256
            else:
1257
                tests.append(LuaTest(k, test_suite.args, test_suite.ini))
×
1258

1259
        test_suite.tests = []
1✔
1260
        # don't sort, command line arguments must be run in
1261
        # the specified order
1262
        for name in test_suite.args.tests:
1✔
1263
            for test in tests:
1✔
1264
                if test.name.find(name) != -1:
1!
1265
                    test_suite.tests.append(test)
1✔
1266

1267
    def get_param(self, param=None):
1✔
1268
        if param is not None:
×
1269
            return yaml.safe_load(self.admin("box.info." + param,
×
1270
                                  silent=True))[0]
1271
        return yaml.safe_load(self.admin("box.info", silent=True))
×
1272

1273
    def get_lsn(self, node_id):
1✔
1274
        nodes = self.get_param("vclock")
×
NEW
1275
        if type(nodes) is dict and node_id in nodes:
×
1276
            return int(nodes[node_id])
×
NEW
1277
        elif type(nodes) is list and node_id <= len(nodes):
×
1278
            return int(nodes[node_id - 1])
×
1279
        else:
1280
            return -1
×
1281

1282
    def wait_lsn(self, node_id, lsn):
1✔
1283
        while (self.get_lsn(node_id) < lsn):
×
1284
            # print("wait_lsn", node_id, lsn, self.get_param("vclock"))
1285
            gevent.sleep(0.01)
×
1286

1287
    def get_log(self):
1✔
1288
        return TarantoolLog(self.logfile).positioning()
×
1289

1290
    def get_iproto_port(self):
1✔
1291
        # Check the `box.cfg.listen` option, if it wasn't defined, just return.
1292
        res = yaml.safe_load(self.admin('box.cfg.listen', silent=True))[0]
1✔
1293
        if res is None:
1!
1294
            return
×
1295

1296
        # If `box.info.listen` (available for tarantool version >= 2.4.1) gives
1297
        # `nil`, use a simple script intended for tarantool version < 2.4.1 to
1298
        # get the listening socket of the instance. First, the script catches
1299
        # both server (listening) and client (sending) sockets (they usually
1300
        # occur when starting an instance as a replica). Then it finds the
1301
        # listening socket among caught sockets.
1302
        script = """
1✔
1303
            local ffi = require('ffi')
1304
            local socket = require('socket')
1305
            local uri = require('uri')
1306
            local res = box.info.listen
1307
            if res then
1308
                local listen_uri = uri.parse(res)
1309
                return {{host = listen_uri.host, port = listen_uri.service}}
1310
            else
1311
                res = {{}}
1312
                local val = ffi.new('int[1]')
1313
                local len = ffi.new('size_t[1]', ffi.sizeof('int'))
1314
                for fd = 0, 65535 do
1315
                    local addrinfo = socket.internal.name(fd)
1316
                    local is_matched = addrinfo ~= nil and
1317
                        addrinfo.host == '{localhost}' and
1318
                        addrinfo.family == 'AF_INET' and
1319
                        addrinfo.type == 'SOCK_STREAM' and
1320
                        addrinfo.protocol == 'tcp' and
1321
                        type(addrinfo.port) == 'number'
1322
                    if is_matched then
1323
                        local lvl = socket.internal.SOL_SOCKET
1324
                        ffi.C.getsockopt(fd, lvl,
1325
                            socket.internal.SO_OPT[lvl].SO_REUSEADDR.iname,
1326
                            val, len)
1327
                        if val[0] > 0 then
1328
                            res[addrinfo.port] = addrinfo
1329
                        end
1330
                    end
1331
                end
1332
                local l_sockets = {{}}
1333
                local con_timeout = 0.1
1334
                for _, s in pairs(res) do
1335
                    con = socket.tcp_connect(s.host, s.port, con_timeout)
1336
                    if con then
1337
                        con:close()
1338
                        table.insert(l_sockets, s)
1339
                    end
1340
                end
1341
                if #l_sockets ~= 1 then
1342
                    error(("Zero or more than one listening TCP sockets: %s")
1343
                        :format(#l_sockets))
1344
                end
1345
                return {{host = l_sockets[1].host, port = l_sockets[1].port}}
1346
            end
1347
        """.format(localhost=self.localhost)
1348
        res = yaml.safe_load(self.admin(script, silent=True))[0]
1✔
1349
        if res.get('error'):
1!
1350
            color_stdout("Failed to get iproto port: {}\n".format(res['error']),
×
1351
                         schema='error')
1352
            raise TarantoolStartError(self.name)
×
1353

1354
        return int(res['port'])
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc