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

tarantool / test-run / 5717496742

pending completion
5717496742

push

github

ylobankov
Fix crash detection

758 of 1562 branches covered (48.53%)

Branch coverage included in aggregate %.

16 of 16 new or added lines in 3 files covered. (100.0%)

2932 of 4344 relevant lines covered (67.5%)

0.67 hits per line

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

61.75
/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.test import TestRunGreenlet, TestExecutionError
1✔
49

50

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

71

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

392

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

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

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

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

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

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

416

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

422

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

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

437

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

443
    def positioning(self):
1✔
444
        if os.path.exists(self.path):
1✔
445
            with open(self.path, 'r') as f:
1✔
446
                f.seek(0, os.SEEK_END)
1✔
447
                self.log_begin = f.tell()
1✔
448
        return self
1✔
449

450
    def seek_once(self, msg):
1✔
451
        if not os.path.exists(self.path):
×
452
            return -1
×
453
        with open(self.path, 'r') as f:
×
454
            f.seek(self.log_begin, os.SEEK_SET)
×
455
            while True:
456
                log_str = f.readline()
×
457

458
                if not log_str:
×
459
                    return -1
×
460
                pos = log_str.find(msg)
×
461
                if pos != -1:
×
462
                    return pos
×
463

464
    def seek_wait(self, msg, proc=None, name=None, deadline=None):
1✔
465
        timeout = Options().args.server_start_timeout
1✔
466
        while not deadline or time.time() < deadline:
1!
467
            if os.path.exists(self.path):
1!
468
                break
1✔
469
            gevent.sleep(0.001)
×
470
        else:
471
            color_stdout("\nFailed to locate {} logfile within {} "
×
472
                         "seconds\n".format(self.path, timeout), schema='error')
473
            return False
×
474

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

503
        color_stdout("\nFailed to find '{}' pattern in {} logfile within {} "
×
504
                     "seconds\n".format(msg, self.path, timeout),
505
                     schema='error')
506

507
        return False
×
508

509

510
class TarantoolServer(Server):
1✔
511
    default_tarantool = {
1✔
512
        "bin": "tarantool",
513
        "logfile": "tarantool.log",
514
        "pidfile": "tarantool.pid",
515
        "name": "default",
516
        "ctl": "tarantoolctl",
517
    }
518

519
    # ----------------------------PROPERTIES--------------------------------- #
520
    @property
1✔
521
    def name(self):
1✔
522
        if not hasattr(self, '_name') or not self._name:
1!
523
            return self.default_tarantool["name"]
×
524
        return self._name
1✔
525

526
    @name.setter
1✔
527
    def name(self, val):
1✔
528
        self._name = val
1✔
529

530
    @property
1✔
531
    def logfile(self):
1✔
532
        if not hasattr(self, '_logfile') or not self._logfile:
1!
533
            return os.path.join(self.vardir, self.default_tarantool["logfile"])
×
534
        return self._logfile
1✔
535

536
    @logfile.setter
1✔
537
    def logfile(self, val):
1✔
538
        self._logfile = os.path.join(self.vardir, val)
1✔
539

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

546
    @pidfile.setter
1✔
547
    def pidfile(self, val):
1✔
548
        self._pidfile = os.path.join(self.vardir, val)
1✔
549

550
    @property
1✔
551
    def builddir(self):
1✔
552
        if not hasattr(self, '_builddir'):
×
553
            raise ValueError("No build-dir is specified")
×
554
        return self._builddir
×
555

556
    @builddir.setter
1✔
557
    def builddir(self, val):
1✔
558
        if val is None:
×
559
            return
×
560
        self._builddir = os.path.abspath(val)
×
561

562
    @property
1✔
563
    def script_dst(self):
1✔
564
        return os.path.join(self.vardir, os.path.basename(self.script))
1✔
565

566
    @property
1✔
567
    def logfile_pos(self):
1✔
568
        if not hasattr(self, '_logfile_pos'):
1!
569
            self._logfile_pos = None
×
570
        return self._logfile_pos
1✔
571

572
    @logfile_pos.setter
1✔
573
    def logfile_pos(self, val):
1✔
574
        self._logfile_pos = TarantoolLog(val).positioning()
1✔
575

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

582
    @script.setter
1✔
583
    def script(self, val):
1✔
584
        if val is None:
1✔
585
            if hasattr(self, '_script'):
1!
586
                delattr(self, '_script')
×
587
            return
1✔
588
        self._script = os.path.abspath(val)
1✔
589
        self.name = os.path.basename(self._script).rsplit(".", maxsplit=1)[0]
1✔
590

591
    @property
1✔
592
    def _admin(self):
1✔
593
        if not hasattr(self, 'admin'):
1!
594
            self.admin = None
×
595
        return self.admin
1✔
596

597
    @_admin.setter
1✔
598
    def _admin(self, port):
1✔
599
        if hasattr(self, 'admin'):
1!
600
            del self.admin
×
601
        if not hasattr(self, 'tests_type'):
1✔
602
            self.tests_type = 'lua'
1✔
603
        self.admin = CON_SWITCH[self.tests_type](self.localhost, port)
1✔
604

605
    @property
1✔
606
    def _iproto(self):
1✔
607
        if not hasattr(self, 'iproto'):
×
608
            self.iproto = None
×
609
        return self.iproto
×
610

611
    @_iproto.setter
1✔
612
    def _iproto(self, port):
1✔
613
        if hasattr(self, 'iproto'):
1!
614
            del self.iproto
×
615
        self.iproto = BoxConnection(self.localhost, port)
1✔
616

617
    @property
1✔
618
    def log_des(self):
1✔
619
        if not hasattr(self, '_log_des'):
1✔
620
            self._log_des = open(self.logfile, 'ab')
1✔
621
        return self._log_des
1✔
622

623
    @log_des.deleter
1✔
624
    def log_des(self):
1✔
625
        if not hasattr(self, '_log_des'):
1!
626
            return
×
627
        if not self._log_des.closed:
1!
628
            self._log_des.close()
1✔
629
        delattr(self, '_log_des')
1✔
630

631
    @property
1✔
632
    def rpl_master(self):
1✔
633
        if not hasattr(self, '_rpl_master'):
1✔
634
            self._rpl_master = None
1✔
635
        return self._rpl_master
1✔
636

637
    @rpl_master.setter
1✔
638
    def rpl_master(self, val):
1✔
639
        if not isinstance(self, (TarantoolServer, None)):
1!
640
            raise ValueError('Replication master must be Tarantool'
×
641
                             ' Server class, his derivation or None')
642
        self._rpl_master = val
1✔
643

644
    # ----------------------------------------------------------------------- #
645

646
    def __new__(cls, ini=None, *args, **kwargs):
1✔
647
        cls = Server.get_mixed_class(cls, ini)
1✔
648
        return object.__new__(cls)
1✔
649

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

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

734
    @classmethod
1✔
735
    def print_exe(cls):
1✔
736
        color_stdout('Tarantool server information:\n', schema='info')
1✔
737
        if cls.binary:
1!
738
            color_stdout(' | Found executable at {}\n'.format(cls.binary))
1✔
739
        if cls.ctl_path:
1!
740
            color_stdout(' | Found tarantoolctl at {}\n'.format(cls.ctl_path))
1✔
741
        color_stdout('\n' + prefix_each_line(' | ', cls.version()) + '\n',
1✔
742
                     schema='version')
743
        color_stdout('Detected build mode: {}\n'.format(
1✔
744
                     'Debug' if cls.debug else 'Release'), schema='info')
745

746
    def install(self, silent=True):
1✔
747
        if self._start_against_running:
1!
748
            self._iproto = self._start_against_running
×
749
            self._admin = int(self._start_against_running) + 1
×
750
            return
×
751
        color_log('DEBUG: [Instance {}] Installing the server...\n'.format(
1✔
752
            self.name), schema='info')
753
        color_log(' | Found executable at {}\n'.format(self.binary))
1✔
754
        color_log(' | Found tarantoolctl at {}\n'.format(self.ctl_path))
1✔
755
        color_log(' | Creating and populating working directory in '
1✔
756
                  '{}...\n'.format(self.vardir))
757
        if not os.path.exists(self.vardir):
1✔
758
            os.makedirs(self.vardir)
1✔
759
        else:
760
            color_log(' | Found old workdir, deleting...\n')
1✔
761
            self.kill_old_server()
1✔
762
            self.cleanup()
1✔
763
        self.copy_files()
1✔
764

765
        path = os.path.join(self.vardir, self.name + ".c")
1✔
766
        warn_unix_socket(path)
1✔
767
        self._admin = path
1✔
768

769
        if self.use_unix_sockets_iproto:
1!
770
            path = os.path.join(self.vardir, self.name + ".i")
×
771
            warn_unix_socket(path)
×
772
            self.listen_uri = path
×
773
            self._iproto = path
×
774
        else:
775
            self.listen_uri = self.localhost + ':0'
1✔
776

777
        # these sockets will be created by tarantool itself
778
        path = os.path.join(self.vardir, self.name + '.control')
1✔
779
        warn_unix_socket(path)
1✔
780

781
    def deploy(self, silent=True, **kwargs):
1✔
782
        self.install(silent)
1✔
783
        self.start(silent=silent, **kwargs)
1✔
784

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

815
        if self.snapshot_path:
1!
816
            # Copy snapshot to the workdir.
817
            # Usually Tarantool looking for snapshots on start in a current directory
818
            # or in a directories that specified in memtx_dir or vinyl_dir box settings.
819
            # Before running test current directory (workdir) passed to a new instance in
820
            # an environment variable TEST_WORKDIR and then tarantoolctl
821
            # adds to it instance_name and set to memtx_dir and vinyl_dir.
822
            (instance_name, _) = os.path.splitext(os.path.basename(self.script))
×
823
            instance_dir = os.path.join(self.vardir, instance_name)
×
824
            safe_makedirs(instance_dir)
×
825
            snapshot_dest = os.path.join(instance_dir, DEFAULT_SNAPSHOT_NAME)
×
826
            color_log("Copying snapshot {} to {}\n".format(
×
827
                self.snapshot_path, snapshot_dest))
828
            shutil.copy(self.snapshot_path, snapshot_dest)
×
829

830
    def prepare_args(self, args=[]):
1✔
831
        cli_args = [self.ctl_path, 'start',
1✔
832
                    os.path.basename(self.script)] + args
833
        if self.disable_schema_upgrade:
1!
834
            cli_args = [self.binary, '-e',
×
835
                        self.DISABLE_AUTO_UPGRADE] + cli_args
836

837
        return cli_args
1✔
838

839
    def pretest_clean(self):
1✔
840
        # Tarantool servers restarts before each test on the worker.
841
        # Snap and logs are removed within it.
842
        pass
1✔
843

844
    def cleanup(self, *args, **kwargs):
1✔
845
        # For `core = tarantool` tests default worker runs on
846
        # subdirectory created by tarantoolctl by using script name
847
        # from suite.ini file.
848
        super(TarantoolServer, self).cleanup(dirname=self.name,
1✔
849
                                             *args, **kwargs)
850

851
    def start(self, silent=True, wait=True, wait_load=True, rais=True, args=[],
1✔
852
              **kwargs):
853
        if self._start_against_running:
1!
854
            return
×
855
        if self.status == 'started':
1!
856
            if not silent:
×
857
                color_stdout('The server is already started.\n',
×
858
                             schema='lerror')
859
            return
×
860

861
        args = self.prepare_args(args)
1✔
862
        self.pidfile = '%s.pid' % self.name
1✔
863
        self.logfile = '%s.log' % self.name
1✔
864

865
        path = self.script_dst if self.script else \
1✔
866
            os.path.basename(self.binary)
867
        color_log('DEBUG: [Instance {}] Starting the server...\n'.format(
1✔
868
            self.name), schema='info')
869
        color_log(' | ' + path + '\n', schema='path')
1✔
870
        color_log(prefix_each_line(' | ', self.version()) + '\n',
1✔
871
                  schema='version')
872

873
        os.putenv("LISTEN", self.listen_uri)
1✔
874
        os.putenv("ADMIN", self.admin.uri)
1✔
875
        if self.rpl_master:
1✔
876
            os.putenv("MASTER", self.rpl_master.iproto.uri)
1✔
877
        self.logfile_pos = self.logfile
1✔
878

879
        # This is strange, but tarantooctl leans on the PWD
880
        # environment variable, not a real current working
881
        # directory, when it performs search for the
882
        # .tarantoolctl configuration file.
883
        os.environ['PWD'] = self.vardir
1✔
884

885
        # redirect stdout from tarantoolctl and tarantool
886
        os.putenv("TEST_WORKDIR", self.vardir)
1✔
887
        self.process = subprocess.Popen(args,
1✔
888
                                        cwd=self.vardir,
889
                                        stdout=self.log_des,
890
                                        stderr=self.log_des)
891
        del self.log_des
1✔
892

893
        # Restore the actual PWD value.
894
        os.environ['PWD'] = os.getcwd()
1✔
895

896
        # Track non-default server metrics as part of current
897
        # test.
898
        if self.current_test:
1✔
899
            sampler.register_process(self.process.pid, self.current_test.id,
1✔
900
                                     self.name)
901

902
        # gh-19 crash detection
903
        self.crash_detector = TestRunGreenlet(self.crash_detect)
1✔
904
        self.crash_detector.info = "Crash detector: %s" % self.process
1✔
905
        self.crash_detector.start()
1✔
906

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

932
        port = self.admin.port
1✔
933
        self.admin.disconnect()
1✔
934
        self.admin = CON_SWITCH[self.tests_type](self.localhost, port)
1✔
935

936
        if not self.use_unix_sockets_iproto:
1!
937
            if wait and wait_load and not self.crash_expected:
1!
938
                self._iproto = self.get_iproto_port()
1✔
939

940
        self.status = 'started'
1✔
941

942
        # Verify that the schema actually was not upgraded.
943
        if self.disable_schema_upgrade:
1!
944
            expected_version = extract_schema_from_snapshot(self.snapshot_path)
×
945
            actual_version = tuple(yaml.safe_load(self.admin.execute(
×
946
                'box.space._schema:get{"version"}'))[0][1:])
947
            if expected_version != actual_version:
×
948
                color_stdout('Schema version check fails: expected '
×
949
                             '{}, got {}\n'.format(expected_version,
950
                                                   actual_version),
951
                             schema='error')
952
                raise TarantoolStartError(self.name)
×
953

954
    def crash_detect(self):
1✔
955
        if self.crash_expected:
1!
956
            return
×
957

958
        while self.process.returncode is None:
1✔
959
            self.process.poll()
1✔
960
            if self.process.returncode is None:
1✔
961
                gevent.sleep(0.1)
1✔
962

963
        if self.process.returncode in [0, -signal.SIGABRT, -signal.SIGKILL, -signal.SIGTERM]:
1!
964
            return
1✔
965

966
        self.kill_current_test()
×
967

968
        if not os.path.exists(self.logfile):
×
969
            return
×
970

971
        if not self.current_test.is_crash_reported:
×
972
            self.current_test.is_crash_reported = True
×
973
            self.crash_grep()
×
974

975
    def crash_grep(self):
1✔
976
        print_log_lines = 15
×
977
        assert_fail_re = re.compile(r'^.*: Assertion .* failed\.$')
×
978

979
        # find and save backtrace or assertion fail
980
        assert_lines = list()
×
981
        bt = list()
×
982
        with open(self.logfile, 'r') as log:
×
983
            lines = log.readlines()
×
984
            for rpos, line in enumerate(reversed(lines)):
×
985
                if line.startswith('Segmentation fault'):
×
986
                    bt = lines[-rpos - 1:]
×
987
                    break
×
988
                if assert_fail_re.match(line):
×
989
                    pos = len(lines) - rpos
×
990
                    assert_lines = lines[max(0, pos - print_log_lines):pos]
×
991
                    break
×
992
            else:
993
                bt = list()
×
994

995
        # print insident meat
996
        if self.process.returncode < 0:
×
997
            color_stdout('\n\n[Instance "%s" killed by signal: %d (%s)]\n' % (
×
998
                self.name, -self.process.returncode,
999
                signame(-self.process.returncode)), schema='error')
1000
        else:
1001
            color_stdout('\n\n[Instance "%s" returns with non-zero exit code: '
×
1002
                         '%d]\n' % (self.name, self.process.returncode),
1003
                         schema='error')
1004

1005
        # print assert line if any and return
1006
        if assert_lines:
×
1007
            color_stdout('Found assertion fail in the results file '
×
1008
                         '[%s]:\n' % self.logfile,
1009
                         schema='error')
1010
            sys.stderr.flush()
×
1011
            for line in assert_lines:
×
1012
                sys.stderr.write(line)
×
1013
            sys.stderr.flush()
×
1014
            return
×
1015

1016
        # print backtrace if any
1017
        sys.stderr.flush()
×
1018
        for trace in bt:
×
1019
            sys.stderr.write(trace)
×
1020

1021
        # print log otherwise (if backtrace was not found)
1022
        if not bt:
×
1023
            self.print_log(print_log_lines)
×
1024
        sys.stderr.flush()
×
1025

1026
    def kill_current_test(self):
1✔
1027
        """ Unblock save_join() call inside LuaTest.execute(), which doing
1028
            necessary servers/greenlets clean up.
1029
        """
1030
        # current_test_greenlet is None for PythonTest
1031
        if self.current_test.current_test_greenlet:
×
1032
            gevent.kill(self.current_test.current_test_greenlet)
×
1033

1034
    def wait_stop(self):
1✔
1035
        self.process.wait()
1✔
1036

1037
    def stop(self, silent=True, signal=signal.SIGTERM):
1✔
1038
        """ Kill tarantool server using specified signal (SIGTERM by default)
1039

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

1072
            # Waiting for stopping the server. If the timeout
1073
            # reached, send SIGKILL.
1074
            timeout = 5
1✔
1075

1076
            def kill():
1✔
1077
                qa_notice('The server \'{}\' does not stop during {} '
×
1078
                          'seconds after the {} ({}) signal.\n'
1079
                          'Info: {}\n'
1080
                          'Sending SIGKILL...'.format(
1081
                              self.name, timeout, signal, signame(signal),
1082
                              format_process(self.process.pid)))
1083
                try:
×
1084
                    self.process.kill()
×
1085
                except OSError:
×
1086
                    pass
×
1087

1088
            timer = Timer(timeout, kill)
1✔
1089
            timer.start()
1✔
1090
            if self.crash_detector is not None:
1!
1091
                save_join(self.crash_detector)
1✔
1092
            self.wait_stop()
1✔
1093
            timer.cancel()
1✔
1094

1095
        self.status = None
1✔
1096
        if re.search(r'^/', str(self._admin.port)):
1!
1097
            if os.path.exists(self._admin.port):
1!
1098
                os.unlink(self._admin.port)
1✔
1099

1100
    def restart(self):
1✔
1101
        self.stop()
×
1102
        self.start()
×
1103

1104
    def kill_old_server(self, silent=True):
1✔
1105
        pid = self.read_pidfile()
1✔
1106
        if pid == -1:
1!
1107
            return False
1✔
1108
        if not silent:
×
1109
            color_stdout(
×
1110
                '    Found old server, pid {0}, killing ...'.format(pid),
1111
                schema='info'
1112
            )
1113
        else:
1114
            color_log('    Found old server, pid {0}, killing ...'.format(pid),
×
1115
                      schema='info')
1116
        try:
×
1117
            os.kill(pid, signal.SIGTERM)
×
1118
        except OSError:
×
1119
            pass
×
1120
        self.wait_until_stopped(pid)
×
1121
        return True
×
1122

1123
    def wait_load(self, deadline):
1✔
1124
        """Wait until the server log file is matched the entry pattern
1125

1126
        If the entry pattern couldn't be found in a log file until a timeout
1127
        is up, it will raise a TarantoolStartError exception.
1128
        """
1129
        msg = 'entering the event loop|will retry binding|hot standby mode'
1✔
1130
        p = self.process if not self.gdb and not self.lldb else None
1✔
1131
        if not self.logfile_pos.seek_wait(msg, p, self.name, deadline):
1!
1132
            raise TarantoolStartError(
×
1133
                self.name, Options().args.server_start_timeout)
1134

1135
    def wait_until_started(self, wait_load=True, deadline=None):
1✔
1136
        """ Wait until server is started.
1137

1138
        Server consists of two parts:
1139
        1) wait until server is listening on sockets
1140
        2) wait until server tells us his status
1141

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

1180
    def wait_until_stopped(self, pid):
1✔
1181
        while True:
1182
            try:
×
1183
                gevent.sleep(0.01)
×
1184
                os.kill(pid, 0)
×
1185
                continue
×
1186
            except OSError:
×
1187
                break
×
1188

1189
    def read_pidfile(self):
1✔
1190
        pid = -1
1✔
1191
        if os.path.exists(self.pidfile):
1!
1192
            try:
×
1193
                with open(self.pidfile) as f:
×
1194
                    pid = int(f.read())
×
1195
            except Exception:
×
1196
                pass
×
1197
        return pid
1✔
1198

1199
    def test_option_get(self, option_list_str, silent=False):
1✔
1200
        args = [self.binary] + shlex.split(option_list_str)
×
1201
        if not silent:
×
1202
            print(" ".join([os.path.basename(self.binary)] + args[1:]))
×
1203
        output = subprocess.Popen(args,
×
1204
                                  cwd=self.vardir,
1205
                                  stdout=subprocess.PIPE,
1206
                                  stderr=subprocess.STDOUT).stdout.read()
1207
        return bytes_to_str(output)
×
1208

1209
    def test_option(self, option_list_str):
1✔
1210
        print(self.test_option_get(option_list_str))
×
1211

1212
    @staticmethod
1✔
1213
    def find_tests(test_suite, suite_path):
1✔
1214
        test_suite.ini['suite'] = suite_path
1✔
1215

1216
        def get_tests(*patterns):
1✔
1217
            res = []
1✔
1218
            for pattern in patterns:
1✔
1219
                path_pattern = os.path.join(suite_path, pattern)
1✔
1220
                res.extend(sorted(glob.glob(path_pattern)))
1✔
1221
            return Server.exclude_tests(res, test_suite.args.exclude)
1✔
1222

1223
        # Add Python tests.
1224
        tests = [PythonTest(k, test_suite.args, test_suite.ini)
1✔
1225
                 for k in get_tests("*.test.py")]
1226

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

1233
            def is_correct(run_name):
1✔
1234
                return test_suite.args.conf is None or \
1✔
1235
                    test_suite.args.conf == run_name
1236

1237
            if runs:
1!
1238
                tests.extend([LuaTest(
1✔
1239
                    k,
1240
                    test_suite.args,
1241
                    test_suite.ini,
1242
                    runs[r],
1243
                    r
1244
                ) for r in runs.keys() if is_correct(r)])
1245
            else:
1246
                tests.append(LuaTest(k, test_suite.args, test_suite.ini))
×
1247

1248
        test_suite.tests = []
1✔
1249
        # don't sort, command line arguments must be run in
1250
        # the specified order
1251
        for name in test_suite.args.tests:
1✔
1252
            for test in tests:
1✔
1253
                if test.name.find(name) != -1:
1!
1254
                    test_suite.tests.append(test)
1✔
1255

1256
    def get_param(self, param=None):
1✔
1257
        if param is not None:
×
1258
            return yaml.safe_load(self.admin("box.info." + param,
×
1259
                                  silent=True))[0]
1260
        return yaml.safe_load(self.admin("box.info", silent=True))
×
1261

1262
    def get_lsn(self, node_id):
1✔
1263
        nodes = self.get_param("vclock")
×
1264
        if type(nodes) == dict and node_id in nodes:
×
1265
            return int(nodes[node_id])
×
1266
        elif type(nodes) == list and node_id <= len(nodes):
×
1267
            return int(nodes[node_id - 1])
×
1268
        else:
1269
            return -1
×
1270

1271
    def wait_lsn(self, node_id, lsn):
1✔
1272
        while (self.get_lsn(node_id) < lsn):
×
1273
            # print("wait_lsn", node_id, lsn, self.get_param("vclock"))
1274
            gevent.sleep(0.01)
×
1275

1276
    def get_log(self):
1✔
1277
        return TarantoolLog(self.logfile).positioning()
×
1278

1279
    def get_iproto_port(self):
1✔
1280
        # Check the `box.cfg.listen` option, if it wasn't defined, just return.
1281
        res = yaml.safe_load(self.admin('box.cfg.listen', silent=True))[0]
1✔
1282
        if res is None:
1!
1283
            return
×
1284

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

1343
        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