clickhouse-test 41.1 KB
Newer Older
A
Azat Khuzhin 已提交
1 2
#!/usr/bin/env python3

3
import sys
4 5 6
import os
import os.path
import re
A
alesapin 已提交
7
import json
8 9 10 11

from argparse import ArgumentParser
from argparse import FileType
from pprint import pprint
12
import shlex
13
import subprocess
14 15 16 17
from subprocess import check_call
from subprocess import Popen
from subprocess import PIPE
from subprocess import CalledProcessError
18
from subprocess import TimeoutExpired
19
from datetime import datetime
A
Alexey Milovidov 已提交
20
from time import time, sleep
21
from errno import ESRCH
22 23 24 25
try:
    import termcolor
except ImportError:
    termcolor = None
26
from random import random
A
Azat Khuzhin 已提交
27
import subprocess
P
proller 已提交
28
import multiprocessing
P
proller 已提交
29
from contextlib import closing
30

31

A
alesapin 已提交
32
MESSAGES_TO_RETRY = [
33
    "DB::Exception: ZooKeeper session has been expired",
A
alesapin 已提交
34
    "Coordination::Exception: Connection loss",
A
Alexey Milovidov 已提交
35
    "Operation timed out",
A
Alexey Milovidov 已提交
36
    "ConnectionPoolWithFailover: Connection failed at try",
A
alesapin 已提交
37 38
]

39

40 41 42
def json_minify(string):
    """
    Removes all js-style comments from json string. Allows to have comments in skip_list.json.
A
alexey-milovidov 已提交
43
    The code taken from https://github.com/getify/JSON.minify/tree/python under the MIT license.
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
    """

    tokenizer = re.compile('"|(/\*)|(\*/)|(//)|\n|\r')
    end_slashes_re = re.compile(r'(\\)*$')

    in_string = False
    in_multi = False
    in_single = False

    new_str = []
    index = 0

    for match in re.finditer(tokenizer, string):
        if not (in_multi or in_single):
            tmp = string[index:match.start()]
            new_str.append(tmp)
        else:
            # Replace comments with white space so that the JSON parser reports
            # the correct column numbers on parsing errors.
            new_str.append(' ' * (match.start() - index))

        index = match.end()
        val = match.group()

        if val == '"' and not (in_multi or in_single):
            escaped = end_slashes_re.search(string, 0, match.start())

            # start of string or unescaped quote character to end string
            if not in_string or (escaped is None or len(escaped.group()) % 2 == 0):  # noqa
                in_string = not in_string
            index -= 1  # include " character in next catch
        elif not (in_string or in_multi or in_single):
            if val == '/*':
                in_multi = True
            elif val == '//':
                in_single = True
        elif val == '*/' and in_multi and not (in_string or in_single):
            in_multi = False
            new_str.append(' ' * len(val))
        elif val in '\r\n' and not (in_multi or in_string) and in_single:
            in_single = False
        elif not in_multi or in_single:  # noqa
            new_str.append(val)

        if val in '\r\n':
            new_str.append(val)
        elif in_multi or in_single:
            new_str.append(' ' * len(val))

    new_str.append(string[index:])
    return ''.join(new_str)


97 98 99 100 101 102
def remove_control_characters(s):
    """
    https://github.com/html5lib/html5lib-python/issues/96#issuecomment-43438438
    """
    def str_to_int(s, default, base=10):
        if int(s, base) < 0x10000:
A
Azat Khuzhin 已提交
103
            return chr(int(s, base))
104
        return default
105 106 107
    s = re.sub(r"&#(\d+);?", lambda c: str_to_int(c.group(1), c.group(0)), s)
    s = re.sub(r"&#[xX]([0-9a-fA-F]+);?", lambda c: str_to_int(c.group(1), c.group(0), base=16), s)
    s = re.sub(r"[\x00-\x08\x0b\x0e-\x1f\x7f]", "", s)
108 109
    return s

110
def get_db_engine(args):
111 112 113
    if args.db_engine:
        return " ENGINE=" + args.db_engine
    return ""   # Will use default engine
114 115

def run_single_test(args, ext, server_logs_level, client_options, case_file, stdout_file, stderr_file):
116 117
    # print(client_options)

118
    start_time = datetime.now()
119 120 121 122 123 124 125 126 127 128 129 130 131 132
    if args.database:
        database = args.database
        os.environ.setdefault("CLICKHOUSE_DATABASE", database)

    else:
        # If --database is not specified, we will create temporary database with unique name
        # And we will recreate and drop it for each test
        def random_str(length=6):
            import random
            import string
            alphabet = string.ascii_lowercase + string.digits
            return ''.join(random.choice(alphabet) for _ in range(length))
        database = 'test_{suffix}'.format(suffix=random_str())

A
Azat Khuzhin 已提交
133
        clickhouse_proc_create = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True)
134 135 136 137 138
        try:
            clickhouse_proc_create.communicate(("CREATE DATABASE " + database + get_db_engine(args)), timeout=args.timeout)
        except TimeoutExpired:
            total_time = (datetime.now() - start_time).total_seconds()
            return clickhouse_proc_create, "", "Timeout creating database {} before test".format(database), total_time
139 140 141

        os.environ["CLICKHOUSE_DATABASE"] = database

142
    params = {
143
        'client': args.client + ' --database=' + database,
144 145 146 147 148 149 150
        'logs_level': server_logs_level,
        'options': client_options,
        'test': case_file,
        'stdout': stdout_file,
        'stderr': stderr_file,
    }

151 152
    pattern = '{test} > {stdout} 2> {stderr}'

A
alesapin 已提交
153
    if ext == '.sql':
154 155 156
        pattern = "{client} --send_logs_level={logs_level} --testmode --multiquery {options} < " + pattern

    command = pattern.format(**params)
M
typo  
myrrc 已提交
157

M
myrrc 已提交
158
    # print(command)
A
alesapin 已提交
159

160
    proc = Popen(command, shell=True, env=os.environ)
M
typo  
myrrc 已提交
161

A
alesapin 已提交
162 163 164
    while (datetime.now() - start_time).total_seconds() < args.timeout and proc.poll() is None:
        sleep(0.01)

165
    if not args.database:
A
Azat Khuzhin 已提交
166
        clickhouse_proc_create = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True)
A
Better  
alesapin 已提交
167
        seconds_left = max(args.timeout - (datetime.now() - start_time).total_seconds(), 10)
168 169 170
        try:
            clickhouse_proc_create.communicate(("DROP DATABASE " + database), timeout=seconds_left)
        except TimeoutExpired:
A
alesapin 已提交
171 172 173 174 175 176 177
            # kill test process because it can also hung
            if proc.returncode is None:
                try:
                    proc.kill()
                except OSError as e:
                    if e.errno != ESRCH:
                        raise
A
alesapin 已提交
178 179

            total_time = (datetime.now() - start_time).total_seconds()
180
            return clickhouse_proc_create, "", "Timeout dropping database {} after test".format(database), total_time
181

182 183
    total_time = (datetime.now() - start_time).total_seconds()

A
alexey-milovidov 已提交
184
    # Normalize randomized database names in stdout, stderr files.
185 186
    os.system("LC_ALL=C sed -i -e 's/{test_db}/default/g' {file}".format(test_db=database, file=stdout_file))
    os.system("LC_ALL=C sed -i -e 's/{test_db}/default/g' {file}".format(test_db=database, file=stderr_file))
187

A
Azat Khuzhin 已提交
188 189 190 191
    stdout = open(stdout_file, 'rb').read() if os.path.exists(stdout_file) else b''
    stdout = str(stdout, errors='replace', encoding='utf-8')
    stderr = open(stderr_file, 'rb').read() if os.path.exists(stderr_file) else b''
    stderr = str(stderr, errors='replace', encoding='utf-8')
A
alesapin 已提交
192

193
    return proc, stdout, stderr, total_time
A
alesapin 已提交
194

195

A
alesapin 已提交
196 197 198
def need_retry(stderr):
    return any(msg in stderr for msg in MESSAGES_TO_RETRY)

199

200 201
def get_processlist(client_cmd):
    try:
T
tavplubix 已提交
202
        return subprocess.check_output("{} --query 'SHOW PROCESSLIST FORMAT Vertical'".format(client_cmd), shell=True).decode('utf-8')
203 204 205
    except:
        return "" #  server seems dead

206

A
alesapin 已提交
207
# collect server stacktraces using gdb
208
def get_stacktraces_from_gdb(server_pid):
209
    cmd = "gdb -batch -ex 'thread apply all backtrace' -p {}".format(server_pid)
210
    try:
T
tavplubix 已提交
211
        return subprocess.check_output(cmd, shell=True).decode('utf-8')
212
    except Exception as ex:
213 214 215
        return "Error occured while receiving stack traces from gdb: {}".format(str(ex))


A
alesapin 已提交
216
# collect server stacktraces from system.stack_trace table
217
# it does not work in Sandbox
218 219
def get_stacktraces_from_clickhouse(client):
    try:
T
tavplubix 已提交
220 221 222 223
        return subprocess.check_output("{} --allow_introspection_functions=1 --query "
               "\"SELECT arrayStringConcat(arrayMap(x, y -> concat(x, ': ', y), arrayMap(x -> addressToLine(x), trace), "
               "arrayMap(x -> demangle(addressToSymbol(x)), trace)), '\n') as trace "
               "FROM system.stack_trace format Vertical\"".format(client), shell=True).decode('utf-8')
224 225
    except Exception as ex:
        return "Error occured while receiving stack traces from client: {}".format(str(ex))
226

227

228
def get_server_pid(server_tcp_port):
229
    cmd = "lsof -i tcp:{port} -s tcp:LISTEN -Fp | awk '/^p[0-9]+$/{{print substr($0, 2)}}'".format(port=server_tcp_port)
230 231 232
    try:
        output = subprocess.check_output(cmd, shell=True)
        if output:
233
            return int(output)
234 235 236 237 238
        else:
            return None # server dead
    except Exception as ex:
        return None

239

P
proller 已提交
240
def colored(text, args, color=None, on_color=None, attrs=None):
241
       if termcolor and (sys.stdout.isatty() or args.force_color):
P
proller 已提交
242 243 244 245
           return termcolor.colored(text, color, on_color, attrs)
       else:
           return text

246

P
proller 已提交
247 248
SERVER_DIED = False
exit_code = 0
A
Alexey Milovidov 已提交
249
stop_time = None
P
proller 已提交
250

251

252
# def run_tests_array(all_tests, suite, suite_dir, suite_tmp_dir, run_total):
253
def run_tests_array(all_tests_with_params):
A
alesapin 已提交
254
    all_tests, suite, suite_dir, suite_tmp_dir = all_tests_with_params
P
proller 已提交
255
    global exit_code
P
proller 已提交
256
    global SERVER_DIED
A
Alexey Milovidov 已提交
257
    global stop_time
258

P
proller 已提交
259 260 261 262 263 264 265 266 267 268 269 270 271
    OP_SQUARE_BRACKET = colored("[", args, attrs=['bold'])
    CL_SQUARE_BRACKET = colored("]", args, attrs=['bold'])

    MSG_FAIL = OP_SQUARE_BRACKET + colored(" FAIL ", args, "red", attrs=['bold']) + CL_SQUARE_BRACKET
    MSG_UNKNOWN = OP_SQUARE_BRACKET + colored(" UNKNOWN ", args, "yellow", attrs=['bold']) + CL_SQUARE_BRACKET
    MSG_OK = OP_SQUARE_BRACKET + colored(" OK ", args, "green", attrs=['bold']) + CL_SQUARE_BRACKET
    MSG_SKIPPED = OP_SQUARE_BRACKET + colored(" SKIPPED ", args, "cyan", attrs=['bold']) + CL_SQUARE_BRACKET

    passed_total = 0
    skipped_total = 0
    failures_total = 0
    failures = 0
    failures_chain = 0
272

273 274
    client_options = get_additional_client_options(args)

275 276
    def print_test_time(test_time):
        if args.print_time:
277 278 279
            return " {0:.2f} sec.".format(test_time)
        else:
            return ''
280

281 282
    if len(all_tests):
        print("\nRunning {} {} tests.".format(len(all_tests), suite) + "\n")
A
alesapin 已提交
283

284
    for case in all_tests:
P
proller 已提交
285 286
        if SERVER_DIED:
            break
A
alesapin 已提交
287

A
Alexey Milovidov 已提交
288 289 290 291
        if stop_time and time() > stop_time:
            print("\nStop tests run because global time limit is exceeded.\n")
            break

P
proller 已提交
292 293 294 295
        case_file = os.path.join(suite_dir, case)
        (name, ext) = os.path.splitext(case)

        try:
296 297 298 299 300 301 302 303 304 305 306
            status = ''
            is_concurrent = multiprocessing.current_process().name != "MainProcess";
            if not is_concurrent:
                sys.stdout.flush()
                sys.stdout.write("{0:72}".format(name + ": "))
                # This flush is needed so you can see the test name of the long
                # running test before it will finish. But don't do it in parallel
                # mode, so that the lines don't mix.
                sys.stdout.flush()
            else:
                status = "{0:72}".format(name + ": ");
P
proller 已提交
307 308

            if args.skip and any(s in name for s in args.skip):
309
                status += MSG_SKIPPED + " - skip\n"
P
proller 已提交
310
                skipped_total += 1
311 312
            elif not args.zookeeper and ('zookeeper' in name
                    or 'replica' in name):
313
                status += MSG_SKIPPED + " - no zookeeper\n"
P
proller 已提交
314
                skipped_total += 1
A
akuzm 已提交
315 316 317
            elif not args.shard and ('shard' in name
                    or 'distributed' in name
                    or 'global' in name):
318
                status += MSG_SKIPPED + " - no shard\n"
P
proller 已提交
319
                skipped_total += 1
C
CurtizJ 已提交
320 321 322 323 324
            elif not args.no_long and ('long' in name
                    # Tests for races and deadlocks usually are runned in loop
                    #  for significant amount of time
                    or 'deadlock' in name
                    or 'race' in name):
325
                status += MSG_SKIPPED + " - no long\n"
P
proller 已提交
326 327 328 329 330 331
                skipped_total += 1
            else:
                disabled_file = os.path.join(suite_dir, name) + '.disabled'

                if os.path.exists(disabled_file) and not args.disabled:
                    message = open(disabled_file, 'r').read()
332
                    status += MSG_SKIPPED + " - " + message + "\n"
P
proller 已提交
333 334 335
                else:

                    if args.testname:
A
Azat Khuzhin 已提交
336
                        clickhouse_proc = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True)
337
                        clickhouse_proc.communicate(("SELECT 'Running test {suite}/{case} from pid={pid}';".format(pid = os.getpid(), case = case, suite = suite)), timeout=10)
P
proller 已提交
338

339 340
                        if clickhouse_proc.returncode != 0:
                            failures += 1
A
alexey-milovidov 已提交
341
                            print("Server does not respond to health check")
342 343 344
                            SERVER_DIED = True
                            break

P
proller 已提交
345 346 347 348
                    reference_file = os.path.join(suite_dir, name) + '.reference'
                    stdout_file = os.path.join(suite_tmp_dir, name) + '.stdout'
                    stderr_file = os.path.join(suite_tmp_dir, name) + '.stderr'

349
                    proc, stdout, stderr, total_time = run_single_test(args, ext, server_logs_level, client_options, case_file, stdout_file, stderr_file)
M
typo  
myrrc 已提交
350

P
proller 已提交
351 352 353 354 355 356 357 358
                    if proc.returncode is None:
                        try:
                            proc.kill()
                        except OSError as e:
                            if e.errno != ESRCH:
                                raise

                        failures += 1
359 360 361
                        status += MSG_FAIL
                        status += print_test_time(total_time)
                        status += " - Timeout!\n"
362
                        if stderr:
363
                            status += stderr
P
proller 已提交
364 365 366
                    else:
                        counter = 1
                        while proc.returncode != 0 and need_retry(stderr):
367
                            proc, stdout, stderr, total_time = run_single_test(args, ext, server_logs_level, client_options, case_file, stdout_file, stderr_file)
P
proller 已提交
368 369 370 371 372 373 374 375
                            sleep(2**counter)
                            counter += 1
                            if counter > 6:
                                break

                        if proc.returncode != 0:
                            failures += 1
                            failures_chain += 1
376 377
                            status += MSG_FAIL
                            status += print_test_time(total_time)
378
                            status += ' - return code {}\n'.format(proc.returncode)
P
proller 已提交
379 380

                            if stderr:
381
                                status += stderr
P
proller 已提交
382

M
typo  
myrrc 已提交
383
                            # Stop on fatal errors like segmentation fault. They are sent to client via logs.
A
Alexey Milovidov 已提交
384 385 386
                            if ' <Fatal> ' in stderr:
                                SERVER_DIED = True

P
proller 已提交
387 388 389
                            if args.stop and ('Connection refused' in stderr or 'Attempt to read after eof' in stderr) and not 'Received exception from server' in stderr:
                                SERVER_DIED = True

390
                            if os.path.isfile(stdout_file):
391 392 393 394
                                status += ", result:\n\n"
                                status += '\n'.join(
                                    open(stdout_file).read().split('\n')[:100])
                                status += '\n';
395

P
proller 已提交
396 397 398
                        elif stderr:
                            failures += 1
                            failures_chain += 1
399 400 401 402
                            status += MSG_FAIL
                            status += print_test_time(total_time)
                            status += " - having stderror:\n{}\n".format(
                                '\n'.join(stderr.split('\n')[:100]))
P
proller 已提交
403 404 405
                        elif 'Exception' in stdout:
                            failures += 1
                            failures_chain += 1
406 407 408 409
                            status += MSG_FAIL
                            status += print_test_time(total_time)
                            status += " - having exception:\n{}\n".format(
                                '\n'.join(stdout.split('\n')[:100]))
P
proller 已提交
410
                        elif not os.path.isfile(reference_file):
411 412 413
                            status += MSG_UNKNOWN
                            status += print_test_time(total_time)
                            status += " - no reference file\n"
P
proller 已提交
414
                        else:
A
Azat Khuzhin 已提交
415
                            result_is_different = subprocess.call(['diff', '-q', reference_file, stdout_file], stdout=PIPE)
P
proller 已提交
416 417

                            if result_is_different:
A
Azat Khuzhin 已提交
418
                                diff = Popen(['diff', '-U', str(args.unified), reference_file, stdout_file], stdout=PIPE, universal_newlines=True).communicate()[0]
P
proller 已提交
419
                                failures += 1
420 421 422
                                status += MSG_FAIL
                                status += print_test_time(total_time)
                                status += " - result differs with reference:\n{}\n".format(diff)
P
proller 已提交
423 424 425
                            else:
                                passed_total += 1
                                failures_chain = 0
426 427 428
                                status += MSG_OK
                                status += print_test_time(total_time)
                                status += "\n"
P
proller 已提交
429 430 431 432
                                if os.path.exists(stdout_file):
                                    os.remove(stdout_file)
                                if os.path.exists(stderr_file):
                                    os.remove(stderr_file)
433 434 435

            sys.stdout.write(status)
            sys.stdout.flush()
436
        except KeyboardInterrupt as e:
P
proller 已提交
437
            print(colored("Break tests execution", args, "red"))
438
            raise e
P
proller 已提交
439 440 441 442
        except:
            import traceback
            exc_type, exc_value, tb = sys.exc_info()
            failures += 1
443
            print("{0} - Test internal error: {1}\n{2}\n{3}".format(MSG_FAIL, exc_type.__name__, exc_value, "\n".join(traceback.format_tb(tb, 10))))
P
proller 已提交
444 445 446 447 448 449 450

        if failures_chain >= 20:
            break

    failures_total = failures_total + failures

    if failures_total > 0:
451
        print(colored("\nHaving {failures_total} errors! {passed_total} tests passed. {skipped_total} tests skipped.".format(passed_total = passed_total, skipped_total = skipped_total, failures_total = failures_total), args, "red", attrs=["bold"]))
P
proller 已提交
452 453 454
        exit_code = 1
    else:
        print(colored("\n{passed_total} tests passed. {skipped_total} tests skipped.".format(passed_total = passed_total, skipped_total = skipped_total), args, "green", attrs=["bold"]))
A
alesapin 已提交
455

456

P
proller 已提交
457 458
server_logs_level = "warning"

459

460
def check_server_started(client, retry_count):
461 462
    print("Connecting to ClickHouse server...", end='')
    sys.stdout.flush()
463 464
    while retry_count > 0:
        clickhouse_proc = Popen(shlex.split(client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
A
Azat Khuzhin 已提交
465
        (stdout, stderr) = clickhouse_proc.communicate(b"SELECT 1")
466

A
Azat Khuzhin 已提交
467
        if clickhouse_proc.returncode == 0 and stdout.startswith(b"1"):
468 469 470 471 472 473 474 475
            print(" OK")
            sys.stdout.flush()
            return True

        if clickhouse_proc.returncode == 210:
            # Connection refused, retry
            print('.', end = '')
            sys.stdout.flush()
476 477
            retry_count -= 1
            sleep(0.5)
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
            continue

        # Other kind of error, fail.
        print('')
        print("Client invocation failed with code ", clickhouse_proc.returncode, ": ")
        # We can't print this, because for some reason this is python 2,
        # and args appeared in 3.3. To hell with it.
        # print(''.join(clickhouse_proc.args))
        print("stdout: ")
        print(stdout)
        print("stderr: ")
        print(stderr)
        sys.stdout.flush()
        return False

    print('')
    print('All connection tries failed')
    sys.stdout.flush()
496 497 498 499

    return False


A
alesapin 已提交
500 501 502 503 504 505 506 507
class BuildFlags(object):
    THREAD = 'thread-sanitizer'
    ADDRESS = 'address-sanitizer'
    UNDEFINED = 'ub-sanitizer'
    MEMORY = 'memory-sanitizer'
    DEBUG = 'debug-build'
    UNBUNDLED = 'unbundled-build'
    RELEASE = 'release-build'
508
    DATABASE_ORDINARY = 'database-ordinary'
A
alesapin 已提交
509 510 511 512 513
    POLYMORPHIC_PARTS = 'polymorphic-parts'


def collect_build_flags(client):
    clickhouse_proc = Popen(shlex.split(client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
A
Azat Khuzhin 已提交
514
    (stdout, stderr) = clickhouse_proc.communicate(b"SELECT value FROM system.build_options WHERE name = 'CXX_FLAGS'")
A
alesapin 已提交
515 516 517
    result = []

    if clickhouse_proc.returncode == 0:
A
Azat Khuzhin 已提交
518
        if b'-fsanitize=thread' in stdout:
A
alesapin 已提交
519
            result.append(BuildFlags.THREAD)
A
Azat Khuzhin 已提交
520
        elif b'-fsanitize=address' in stdout:
A
alesapin 已提交
521
            result.append(BuildFlags.ADDRESS)
A
Azat Khuzhin 已提交
522
        elif b'-fsanitize=undefined' in stdout:
A
alesapin 已提交
523
            result.append(BuildFlags.UNDEFINED)
A
Azat Khuzhin 已提交
524
        elif b'-fsanitize=memory' in stdout:
A
alesapin 已提交
525 526
            result.append(BuildFlags.MEMORY)
    else:
M
typo  
myrrc 已提交
527
        raise Exception("Cannot get information about build from server errorcode {}, stderr {}".format(clickhouse_proc.returncode, stderr))
A
alesapin 已提交
528 529

    clickhouse_proc = Popen(shlex.split(client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
A
Azat Khuzhin 已提交
530
    (stdout, stderr) = clickhouse_proc.communicate(b"SELECT value FROM system.build_options WHERE name = 'BUILD_TYPE'")
A
alesapin 已提交
531 532

    if clickhouse_proc.returncode == 0:
A
Azat Khuzhin 已提交
533
        if b'Debug' in stdout:
A
alesapin 已提交
534
            result.append(BuildFlags.DEBUG)
A
Azat Khuzhin 已提交
535
        elif b'RelWithDebInfo' in stdout or b'Release' in stdout:
A
alesapin 已提交
536 537
            result.append(BuildFlags.RELEASE)
    else:
M
typo  
myrrc 已提交
538
        raise Exception("Cannot get information about build from server errorcode {}, stderr {}".format(clickhouse_proc.returncode, stderr))
A
alesapin 已提交
539 540

    clickhouse_proc = Popen(shlex.split(client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
A
Azat Khuzhin 已提交
541
    (stdout, stderr) = clickhouse_proc.communicate(b"SELECT value FROM system.build_options WHERE name = 'UNBUNDLED'")
A
alesapin 已提交
542 543

    if clickhouse_proc.returncode == 0:
A
Azat Khuzhin 已提交
544
        if b'ON' in stdout or b'1' in stdout:
A
alesapin 已提交
545 546
            result.append(BuildFlags.UNBUNDLED)
    else:
M
typo  
myrrc 已提交
547
        raise Exception("Cannot get information about build from server errorcode {}, stderr {}".format(clickhouse_proc.returncode, stderr))
A
alesapin 已提交
548 549

    clickhouse_proc = Popen(shlex.split(client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
A
Azat Khuzhin 已提交
550
    (stdout, stderr) = clickhouse_proc.communicate(b"SELECT value FROM system.settings WHERE name = 'default_database_engine'")
A
alesapin 已提交
551 552

    if clickhouse_proc.returncode == 0:
A
Azat Khuzhin 已提交
553
        if b'Ordinary' in stdout:
554
            result.append(BuildFlags.DATABASE_ORDINARY)
A
alesapin 已提交
555
    else:
M
typo  
myrrc 已提交
556
        raise Exception("Cannot get information about build from server errorcode {}, stderr {}".format(clickhouse_proc.returncode, stderr))
A
alesapin 已提交
557

A
Anton Popov 已提交
558 559 560 561 562 563 564 565 566
    clickhouse_proc = Popen(shlex.split(client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
    (stdout, stderr) = clickhouse_proc.communicate(b"SELECT value FROM system.merge_tree_settings WHERE name = 'min_bytes_for_wide_part'")

    if clickhouse_proc.returncode == 0:
        if stdout == b'0\n':
            result.append(BuildFlags.POLYMORPHIC_PARTS)
    else:
        raise Exception("Cannot get inforamtion about build from server errorcode {}, stderr {}".format(clickhouse_proc.returncode, stderr))

A
alesapin 已提交
567 568 569
    return result


P
proller 已提交
570 571
def main(args):
    global SERVER_DIED
A
Alexey Milovidov 已提交
572
    global stop_time
P
proller 已提交
573 574
    global exit_code
    global server_logs_level
A
alesapin 已提交
575

576
    def is_data_present():
P
proller 已提交
577
        clickhouse_proc = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
A
Azat Khuzhin 已提交
578
        (stdout, stderr) = clickhouse_proc.communicate(b"EXISTS TABLE test.hits")
P
proller 已提交
579
        if clickhouse_proc.returncode != 0:
P
proller 已提交
580
            raise CalledProcessError(clickhouse_proc.returncode, args.client, stderr)
581

A
Azat Khuzhin 已提交
582
        return stdout.startswith(b'1')
583

584
    if not check_server_started(args.client, args.server_check_retries):
M
typo  
myrrc 已提交
585 586
        raise Exception(
            "Server is not responding. Cannot execute 'SELECT 1' query. \
A
Alexander Kuzmenkov 已提交
587
            Note: if you are using split build, you may have to specify -c option.")
M
typo  
myrrc 已提交
588

A
alesapin 已提交
589
    build_flags = collect_build_flags(args.client)
590 591
    if args.antlr:
        build_flags.append('antlr')
M
typo  
myrrc 已提交
592

A
alesapin 已提交
593 594 595
    if args.use_skip_list:
        tests_to_skip_from_list = collect_tests_to_skip(args.skip_list_path, build_flags)
    else:
A
Fixes  
alesapin 已提交
596
        tests_to_skip_from_list = set([])
A
alesapin 已提交
597

A
alesapin 已提交
598 599 600 601
    if args.skip:
        args.skip = set(args.skip) | tests_to_skip_from_list
    else:
        args.skip = tests_to_skip_from_list
602

603 604 605
    if args.use_skip_list and not args.sequential:
        args.sequential = collect_sequential_list(args.skip_list_path)

P
proller 已提交
606 607 608
    base_dir = os.path.abspath(args.queries)
    tmp_dir = os.path.abspath(args.tmp)

609
    # Keep same default values as in queries/shell_config.sh
P
proller 已提交
610
    os.environ.setdefault("CLICKHOUSE_BINARY", args.binary)
611
    #os.environ.setdefault("CLICKHOUSE_CLIENT", args.client)
612
    os.environ.setdefault("CLICKHOUSE_CONFIG", args.configserver)
P
proller 已提交
613 614
    if args.configclient:
        os.environ.setdefault("CLICKHOUSE_CONFIG_CLIENT", args.configclient)
P
proller 已提交
615
    os.environ.setdefault("CLICKHOUSE_TMP", tmp_dir)
616

617
    # Force to print server warnings in stderr
618
    # Shell scripts could change logging level
619 620
    os.environ.setdefault("CLICKHOUSE_CLIENT_SERVER_LOGS_LEVEL", server_logs_level)

A
Alexey Milovidov 已提交
621 622 623 624
    # This code is bad as the time is not monotonic
    if args.global_time_limit:
        stop_time = time() + args.global_time_limit

625
    if args.zookeeper is None:
A
Azat Khuzhin 已提交
626
        code, out = subprocess.getstatusoutput(args.extract_from_config + " --try --config " + args.configserver + ' --key zookeeper | grep . | wc -l')
627 628 629 630 631 632
        try:
            if int(out) > 0:
                args.zookeeper = True
            else:
                args.zookeeper = False
        except ValueError:
633 634 635
            args.zookeeper = False

    if args.shard is None:
A
Azat Khuzhin 已提交
636
        code, out = subprocess.getstatusoutput(args.extract_from_config + " --try --config " + args.configserver + ' --key listen_host | grep -E "127.0.0.2|::"')
P
proller 已提交
637
        if out:
P
proller 已提交
638
            args.shard = True
P
proller 已提交
639 640
        else:
            args.shard = False
641

642
    if args.database and args.database != "test":
A
Azat Khuzhin 已提交
643 644
        clickhouse_proc_create = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True)
        clickhouse_proc_create.communicate(("CREATE DATABASE IF NOT EXISTS " + args.database + get_db_engine(args)))
645

A
Azat Khuzhin 已提交
646 647
    clickhouse_proc_create = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True)
    clickhouse_proc_create.communicate(("CREATE DATABASE IF NOT EXISTS test" + get_db_engine(args)))
648 649 650 651

    def is_test_from_dir(suite_dir, case):
        case_file = os.path.join(suite_dir, case)
        (name, ext) = os.path.splitext(case)
A
Alexander Kuzmenkov 已提交
652 653 654 655
        # We could also test for executable files (os.access(case_file, os.X_OK),
        # but it interferes with 01610_client_spawn_editor.editor, which is invoked
        # as a query editor in the test, and must be marked as executable.
        return os.path.isfile(case_file) and (ext == '.sql' or ext == '.sh' or ext == '.py' or ext == '.expect')
P
proller 已提交
656

657 658 659 660 661
    def sute_key_func(item):
       if args.order == 'random':
             return random()

       if -1 == item.find('_'):
A
Azat Khuzhin 已提交
662
           return 99998, ''
663 664 665 666 667 668

       prefix, suffix = item.split('_', 1)

       try:
           return int(prefix), suffix
       except ValueError:
A
Azat Khuzhin 已提交
669
           return 99997, ''
670

671
    total_tests_run = 0
672
    for suite in sorted(os.listdir(base_dir), key=sute_key_func):
673 674 675 676 677 678 679
        if SERVER_DIED:
            break

        suite_dir = os.path.join(base_dir, suite)
        suite_re_obj = re.search('^[0-9]+_(.*)$', suite)
        if not suite_re_obj: #skip .gitignore and so on
            continue
680 681 682 683 684

        suite_tmp_dir = os.path.join(tmp_dir, suite)
        if not os.path.exists(suite_tmp_dir):
            os.makedirs(suite_tmp_dir)

685 686 687
        suite = suite_re_obj.group(1)
        if os.path.isdir(suite_dir):

P
proller 已提交
688
            if 'stateful' in suite and not args.no_stateful and not is_data_present():
689
                print("Won't run stateful tests because test data wasn't loaded.")
690
                continue
691 692 693
            if 'stateless' in suite and args.no_stateless:
                print("Won't run stateless tests because they were manually disabled.")
                continue
694 695 696
            if 'stateful' in suite and args.no_stateful:
                print("Won't run stateful tests because they were manually disabled.")
                continue
697

P
proller 已提交
698 699 700
            # Reverse sort order: we want run newest test first.
            # And not reverse subtests
            def key_func(item):
701
                if args.order == 'random':
702
                    return random()
703 704 705 706 707 708

                reverse = 1 if args.order == 'asc' else -1

                if -1 == item.find('_'):
                    return 99998

P
proller 已提交
709
                prefix, suffix = item.split('_', 1)
710 711

                try:
A
Alexey Milovidov 已提交
712
                    return reverse * int(prefix)
713 714 715
                except ValueError:
                    return 99997

716
            all_tests = os.listdir(suite_dir)
A
Azat Khuzhin 已提交
717
            all_tests = [case for case in all_tests if is_test_from_dir(suite_dir, case)]
718 719
            if args.test:
                all_tests = [t for t in all_tests if any([re.search(r, t) for r in args.test])]
720
            all_tests.sort(key=key_func)
721

A
alesapin 已提交
722
            jobs = args.jobs
723 724 725 726 727 728 729 730
            parallel_tests = []
            sequential_tests = []
            for test in all_tests:
                if any(s in test for s in args.sequential):
                    sequential_tests.append(test)
                else:
                    parallel_tests.append(test)

A
alesapin 已提交
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748
            if jobs > 1 and len(parallel_tests) > 0:
                print("Found", len(parallel_tests), "parallel tests and", len(sequential_tests), "sequential tests")
                run_n, run_total = args.parallel.split('/')
                run_n = float(run_n)
                run_total = float(run_total)
                tests_n = len(parallel_tests)
                if run_total > tests_n:
                    run_total = tests_n

                if jobs > tests_n:
                    jobs = tests_n
                if jobs > run_total:
                    run_total = jobs

                batch_size = len(parallel_tests) // jobs
                parallel_tests_array = []
                for i in range(0, len(parallel_tests), batch_size):
                    parallel_tests_array.append((parallel_tests[i:i+batch_size], suite, suite_dir, suite_tmp_dir))
P
proller 已提交
749

750
                with closing(multiprocessing.Pool(processes=jobs)) as pool:
751
                    pool.map(run_tests_array, parallel_tests_array)
752

A
alesapin 已提交
753
                run_tests_array((sequential_tests, suite, suite_dir, suite_tmp_dir))
754 755
                total_tests_run += len(sequential_tests) + len(parallel_tests)
            else:
A
alesapin 已提交
756
                run_tests_array((all_tests, suite, suite_dir, suite_tmp_dir))
757
                total_tests_run += len(all_tests)
758

759
    if args.hung_check:
760 761 762 763 764 765 766 767

        # Some queries may execute in background for some time after test was finished. This is normal.
        for n in range(1, 60):
            processlist = get_processlist(args.client)
            if not processlist:
                break
            sleep(1)

768 769 770 771 772 773 774 775
        if processlist:
            print(colored("\nFound hung queries in processlist:", args, "red", attrs=["bold"]))
            print(processlist)

            clickhouse_tcp_port = os.getenv("CLICKHOUSE_PORT_TCP", '9000')
            server_pid = get_server_pid(clickhouse_tcp_port)
            if server_pid:
                print("\nLocated ClickHouse server process {} listening at TCP port {}".format(server_pid, clickhouse_tcp_port))
776 777 778 779 780

                # It does not work in Sandbox
                #print("\nCollecting stacktraces from system.stacktraces table:")
                #print(get_stacktraces_from_clickhouse(args.client))

781 782
                print("\nCollecting stacktraces from all running threads with gdb:")
                print(get_stacktraces_from_gdb(server_pid))
783
            else:
784 785 786 787 788 789 790
                print(
                    colored(
                        "\nUnable to locate ClickHouse server process listening at TCP port {}. "
                        "It must have crashed or exited prematurely!".format(clickhouse_tcp_port),
                        args, "red", attrs=["bold"]))

            exit_code = 1
791
        else:
792
            print(colored("\nNo queries hung.", args, "green", attrs=["bold"]))
793

794 795 796 797
    if total_tests_run == 0:
        print("No tests were run.")
        sys.exit(1)

798 799
    sys.exit(exit_code)

800

A
alesapin 已提交
801
def find_binary(name):
802 803
    if os.path.exists(name) and os.access(name, os.X_OK):
        return True
A
alesapin 已提交
804 805 806 807 808 809
    paths = os.environ.get("PATH").split(':')
    for path in paths:
        if os.access(os.path.join(path, name), os.X_OK):
            return True

    # maybe it wasn't in PATH
P
proller 已提交
810 811 812 813 814 815
    if os.access(os.path.join('/usr/local/bin', name), os.X_OK):
        return True
    if os.access(os.path.join('/usr/bin', name), os.X_OK):
        return True
    return False

816

817
def get_additional_client_options(args):
818 819 820 821 822 823 824 825 826 827 828
    if args.client_option:
        return ' '.join('--' + option for option in args.client_option)

    return ''


def get_additional_client_options_url(args):
    if args.client_option:
        return '&'.join(args.client_option)

    return ''
829 830


A
alesapin 已提交
831 832 833 834 835 836
def collect_tests_to_skip(skip_list_path, build_flags):
    result = set([])
    if not os.path.exists(skip_list_path):
        return result

    with open(skip_list_path, 'r') as skip_list_file:
837 838 839
        content = skip_list_file.read()
        # allows to have comments in skip_list.json
        skip_dict = json.loads(json_minify(content))
A
alesapin 已提交
840 841
        for build_flag in build_flags:
            result |= set(skip_dict[build_flag])
A
alesapin 已提交
842 843 844 845

    if len(result) > 0:
        print("Found file with skip-list {}, {} test will be skipped".format(skip_list_path, len(result)))

A
alesapin 已提交
846 847
    return result

848 849 850 851 852 853 854 855 856

def collect_sequential_list(skip_list_path):
    if not os.path.exists(skip_list_path):
        return set([])

    with open(skip_list_path, 'r') as skip_list_file:
        content = skip_list_file.read()
        # allows to have comments in skip_list.json
        skip_dict = json.loads(json_minify(content))
A
alesapin 已提交
857 858
        if 'parallel' in skip_dict:
            return skip_dict['parallel']
859 860 861
        return set([])


862
if __name__ == '__main__':
863 864 865
    parser=ArgumentParser(description='ClickHouse functional tests')
    parser.add_argument('-q', '--queries', help='Path to queries dir')
    parser.add_argument('--tmp', help='Path to tmp dir')
M
typo  
myrrc 已提交
866 867

    parser.add_argument('-b', '--binary', default='clickhouse',
A
Alexander Kuzmenkov 已提交
868
        help='Path to clickhouse (if monolithic build, clickhouse-server otherwise) binary or name of binary in PATH')
M
typo  
myrrc 已提交
869 870

    parser.add_argument('-c', '--client',
A
Alexander Kuzmenkov 已提交
871
        help='Path to clickhouse-client (if split build, useless otherwise) binary of name of binary in PATH')
M
typo  
myrrc 已提交
872

873 874 875 876 877
    parser.add_argument('--extract_from_config', help='extract-from-config program')
    parser.add_argument('--configclient', help='Client config (if you use not default ports)')
    parser.add_argument('--configserver', default= '/etc/clickhouse-server/config.xml', help='Preprocessed server config')
    parser.add_argument('-o', '--output', help='Output xUnit compliant test report directory')
    parser.add_argument('-t', '--timeout', type=int, default=600, help='Timeout for each test case in seconds')
A
Alexey Milovidov 已提交
878
    parser.add_argument('--global_time_limit', type=int, help='Stop if executing more than specified time (after current test finished)')
879
    parser.add_argument('test', nargs='*', help='Optional test case name regex')
880 881
    parser.add_argument('-d', '--disabled', action='store_true', default=False, help='Also run disabled tests')
    parser.add_argument('--stop', action='store_true', default=None, dest='stop', help='Stop on network errors')
882
    parser.add_argument('--order', default='desc', choices=['asc', 'desc', 'random'], help='Run order')
883 884
    parser.add_argument('--testname', action='store_true', default=None, dest='testname', help='Make query with test name before test run')
    parser.add_argument('--hung-check', action='store_true', default=False)
A
alesapin 已提交
885
    parser.add_argument('--force-color', action='store_true', default=False)
886
    parser.add_argument('--database', help='Database for tests (random name test_XXXXXX by default)')
P
proller 已提交
887
    parser.add_argument('--parallel', default='1/1', help='One parallel test run number/total')
P
proller 已提交
888
    parser.add_argument('-j', '--jobs', default=1, nargs='?', type=int, help='Run all tests in parallel')
889
    parser.add_argument('-U', '--unified', default=3, type=int, help='output NUM lines of unified context')
890
    parser.add_argument('-r', '--server-check-retries', default=30, type=int, help='Num of tries to execute SELECT 1 before tests started')
A
alesapin 已提交
891 892
    parser.add_argument('--skip-list-path', help="Path to skip-list file")
    parser.add_argument('--use-skip-list', action='store_true', default=False, help="Use skip list to skip tests if found")
893
    parser.add_argument('--db-engine', help='Database engine name')
894

895
    parser.add_argument('--antlr', action='store_true', default=False, dest='antlr', help='Use new ANTLR parser in tests')
896
    parser.add_argument('--no-stateless', action='store_true', help='Disable all stateless tests')
897
    parser.add_argument('--no-stateful', action='store_true', help='Disable all stateful tests')
898
    parser.add_argument('--skip', nargs='+', help="Skip these tests")
899
    parser.add_argument('--sequential', nargs='+', help="Run these tests sequentially even if --parallel specified")
900
    parser.add_argument('--no-long', action='store_false', dest='no_long', help='Do not run long tests')
901
    parser.add_argument('--client-option', nargs='+', help='Specify additional client argument')
902
    parser.add_argument('--print-time', action='store_true', dest='print_time', help='Print test time')
903 904 905 906 907 908
    group=parser.add_mutually_exclusive_group(required=False)
    group.add_argument('--zookeeper', action='store_true', default=None, dest='zookeeper', help='Run zookeeper related tests')
    group.add_argument('--no-zookeeper', action='store_false', default=None, dest='zookeeper', help='Do not run zookeeper related tests')
    group=parser.add_mutually_exclusive_group(required=False)
    group.add_argument('--shard', action='store_true', default=None, dest='shard', help='Run sharding related tests (required to clickhouse-server listen 127.0.0.2 127.0.0.3)')
    group.add_argument('--no-shard', action='store_false', default=None, dest='shard', help='Do not run shard related tests')
909 910

    args = parser.parse_args()
911

A
Alexander Kuzmenkov 已提交
912
    if args.queries and not os.path.isdir(args.queries):
A
Alexander Kuzmenkov 已提交
913
        print("Cannot access the specified directory with queries (" + args.queries + ")", file=sys.stderr)
A
Alexander Kuzmenkov 已提交
914 915 916 917
        exit(1)

    # Autodetect the directory with queries if not specified
    if args.queries is None:
918
        args.queries = 'queries'
A
Alexander Kuzmenkov 已提交
919 920 921 922 923 924 925

    if not os.path.isdir(args.queries):
        # If we're running from the repo
        args.queries = os.path.join(os.path.dirname(os.path.abspath( __file__ )), 'queries')

    if not os.path.isdir(args.queries):
        # Next we're going to try some system directories, don't write 'stdout' files into them.
926 927
        if args.tmp is None:
            args.tmp = '/tmp/clickhouse-test'
A
Alexander Kuzmenkov 已提交
928 929 930 931 932 933 934

        args.queries = '/usr/local/share/clickhouse-test/queries'

    if not os.path.isdir(args.queries):
        args.queries = '/usr/share/clickhouse-test/queries'

    if not os.path.isdir(args.queries):
935
        print("Failed to detect path to the queries directory. Please specify it with '--queries' option.", file=sys.stderr)
A
akuzm 已提交
936
        exit(1)
A
alesapin 已提交
937

A
Alexander Kuzmenkov 已提交
938 939
    print("Using queries from '" + args.queries + "' directory")

A
alesapin 已提交
940 941
    if args.skip_list_path is None:
        args.skip_list_path = os.path.join(args.queries, 'skip_list.json')
A
alesapin 已提交
942

943 944 945
    if args.sequential is None:
        args.sequential = set([])

P
proller 已提交
946 947
    if args.tmp is None:
        args.tmp = args.queries
948
    if args.client is None:
A
alesapin 已提交
949
        if find_binary(args.binary + '-client'):
P
proller 已提交
950
            args.client = args.binary + '-client'
M
typo  
myrrc 已提交
951

A
Alexander Kuzmenkov 已提交
952
            print("Using " + args.client + " as client program (expecting split build)")
A
alesapin 已提交
953
        elif find_binary(args.binary):
P
proller 已提交
954
            args.client = args.binary + ' client'
M
typo  
myrrc 已提交
955

A
Alexander Kuzmenkov 已提交
956
            print("Using " + args.client + " as client program (expecting monolithic build)")
957
        else:
M
typo  
myrrc 已提交
958
            print("No 'clickhouse' or 'clickhouse-client' client binary found", file=sys.stderr)
A
alesapin 已提交
959 960 961
            parser.print_help()
            exit(1)

P
proller 已提交
962
        if args.configclient:
963 964 965 966 967
            args.client += ' --config-file=' + args.configclient
        if os.getenv("CLICKHOUSE_HOST"):
            args.client += ' --host=' + os.getenv("CLICKHOUSE_HOST")
        if os.getenv("CLICKHOUSE_PORT_TCP"):
            args.client += ' --port=' + os.getenv("CLICKHOUSE_PORT_TCP")
968 969 970
        if os.getenv("CLICKHOUSE_DATABASE"):
            args.client += ' --database=' + os.getenv("CLICKHOUSE_DATABASE")

971
    if args.client_option:
972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987
        # Set options for client
        if 'CLICKHOUSE_CLIENT_OPT' in os.environ:
           os.environ['CLICKHOUSE_CLIENT_OPT'] += ' '
        else:
           os.environ['CLICKHOUSE_CLIENT_OPT'] = ''

        os.environ['CLICKHOUSE_CLIENT_OPT'] += get_additional_client_options(args)

        # Set options for curl
        if 'CLICKHOUSE_URL_PARAMS' in os.environ:
           os.environ['CLICKHOUSE_URL_PARAMS'] += '&'
        else:
           os.environ['CLICKHOUSE_URL_PARAMS'] = ''

        os.environ['CLICKHOUSE_URL_PARAMS'] += get_additional_client_options_url(args)

988
    if args.antlr:
989 990 991 992
        if 'CLICKHOUSE_CLIENT_OPT' in os.environ:
           os.environ['CLICKHOUSE_CLIENT_OPT'] += ' --use_antlr_parser=1'
        else:
           os.environ['CLICKHOUSE_CLIENT_OPT'] = '--use_antlr_parser=1'
993

P
proller 已提交
994 995 996 997 998 999
    if args.extract_from_config is None:
        if os.access(args.binary + '-extract-from-config', os.X_OK):
            args.extract_from_config = args.binary + '-extract-from-config'
        else:
            args.extract_from_config = args.binary + ' extract-from-config'

P
proller 已提交
1000
    if args.jobs is None:
1001
        args.jobs = multiprocessing.cpu_count()
P
proller 已提交
1002

1003
    main(args)