clickhouse-test 40.0 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 277 278
    def print_test_time(test_time):
        if args.print_time:
            print(" {0:.2f} sec.".format(test_time), end='')

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

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

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

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

        try:
A
alesapin 已提交
294
            sys.stdout.flush()
P
proller 已提交
295 296 297 298 299
            sys.stdout.write("{0:72}".format(name + ": "))

            if args.skip and any(s in name for s in args.skip):
                print(MSG_SKIPPED + " - skip")
                skipped_total += 1
300 301
            elif not args.zookeeper and ('zookeeper' in name
                    or 'replica' in name):
P
proller 已提交
302 303
                print(MSG_SKIPPED + " - no zookeeper")
                skipped_total += 1
A
akuzm 已提交
304 305 306
            elif not args.shard and ('shard' in name
                    or 'distributed' in name
                    or 'global' in name):
P
proller 已提交
307 308
                print(MSG_SKIPPED + " - no shard")
                skipped_total += 1
C
CurtizJ 已提交
309 310 311 312 313
            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):
P
proller 已提交
314 315 316 317 318 319 320 321 322 323 324
                print(MSG_SKIPPED + " - no long")
                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()
                    print(MSG_SKIPPED + " - " + message)
                else:

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

328 329
                        if clickhouse_proc.returncode != 0:
                            failures += 1
A
alexey-milovidov 已提交
330
                            print("Server does not respond to health check")
331 332 333
                            SERVER_DIED = True
                            break

P
proller 已提交
334 335 336 337
                    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'

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

P
proller 已提交
340 341 342 343 344 345 346 347
                    if proc.returncode is None:
                        try:
                            proc.kill()
                        except OSError as e:
                            if e.errno != ESRCH:
                                raise

                        failures += 1
348 349 350
                        print(MSG_FAIL, end='')
                        print_test_time(total_time)
                        print(" - Timeout!")
351 352
                        if stderr:
                            print(stderr)
P
proller 已提交
353 354 355
                    else:
                        counter = 1
                        while proc.returncode != 0 and need_retry(stderr):
356
                            proc, stdout, stderr, total_time = run_single_test(args, ext, server_logs_level, client_options, case_file, stdout_file, stderr_file)
P
proller 已提交
357 358 359 360 361 362 363 364
                            sleep(2**counter)
                            counter += 1
                            if counter > 6:
                                break

                        if proc.returncode != 0:
                            failures += 1
                            failures_chain += 1
365 366 367
                            print(MSG_FAIL, end='')
                            print_test_time(total_time)
                            print(" - return code {}".format(proc.returncode))
P
proller 已提交
368 369

                            if stderr:
A
Azat Khuzhin 已提交
370
                                print(stderr)
P
proller 已提交
371

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

P
proller 已提交
376 377 378
                            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

379 380
                            if os.path.isfile(stdout_file):
                                print(", result:\n")
A
Alexander Kuzmenkov 已提交
381
                                print('\n'.join(open(stdout_file).read().split('\n')[:100]))
382

P
proller 已提交
383 384 385
                        elif stderr:
                            failures += 1
                            failures_chain += 1
386 387
                            print(MSG_FAIL, end='')
                            print_test_time(total_time)
A
Alexander Kuzmenkov 已提交
388 389
                            print(" - having stderror:\n{}".format(
                                '\n'.join(stderr.split('\n')[:100])))
P
proller 已提交
390 391 392
                        elif 'Exception' in stdout:
                            failures += 1
                            failures_chain += 1
393 394
                            print(MSG_FAIL, end='')
                            print_test_time(total_time)
A
Alexander Kuzmenkov 已提交
395 396
                            print(" - having exception:\n{}".format(
                                '\n'.join(stdout.split('\n')[:100])))
P
proller 已提交
397
                        elif not os.path.isfile(reference_file):
398 399 400
                            print(MSG_UNKNOWN, end='')
                            print_test_time(total_time)
                            print(" - no reference file")
P
proller 已提交
401
                        else:
A
Azat Khuzhin 已提交
402
                            result_is_different = subprocess.call(['diff', '-q', reference_file, stdout_file], stdout=PIPE)
P
proller 已提交
403 404

                            if result_is_different:
A
Azat Khuzhin 已提交
405
                                diff = Popen(['diff', '-U', str(args.unified), reference_file, stdout_file], stdout=PIPE, universal_newlines=True).communicate()[0]
P
proller 已提交
406
                                failures += 1
407 408 409
                                print(MSG_FAIL, end='')
                                print_test_time(total_time)
                                print(" - result differs with reference:\n{}".format(diff))
P
proller 已提交
410 411 412
                            else:
                                passed_total += 1
                                failures_chain = 0
413 414 415
                                print(MSG_OK, end='')
                                print_test_time(total_time)
                                print()
P
proller 已提交
416 417 418 419
                                if os.path.exists(stdout_file):
                                    os.remove(stdout_file)
                                if os.path.exists(stderr_file):
                                    os.remove(stderr_file)
420
        except KeyboardInterrupt as e:
P
proller 已提交
421
            print(colored("Break tests execution", args, "red"))
422
            raise e
P
proller 已提交
423 424 425 426
        except:
            import traceback
            exc_type, exc_value, tb = sys.exc_info()
            failures += 1
427
            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 已提交
428 429 430 431 432 433 434

        if failures_chain >= 20:
            break

    failures_total = failures_total + failures

    if failures_total > 0:
435
        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 已提交
436 437 438
        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 已提交
439

440

P
proller 已提交
441 442
server_logs_level = "warning"

443

444
def check_server_started(client, retry_count):
445 446
    print("Connecting to ClickHouse server...", end='')
    sys.stdout.flush()
447 448
    while retry_count > 0:
        clickhouse_proc = Popen(shlex.split(client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
A
Azat Khuzhin 已提交
449
        (stdout, stderr) = clickhouse_proc.communicate(b"SELECT 1")
450

A
Azat Khuzhin 已提交
451
        if clickhouse_proc.returncode == 0 and stdout.startswith(b"1"):
452 453 454 455 456 457 458 459
            print(" OK")
            sys.stdout.flush()
            return True

        if clickhouse_proc.returncode == 210:
            # Connection refused, retry
            print('.', end = '')
            sys.stdout.flush()
460 461
            retry_count -= 1
            sleep(0.5)
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
            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()
480 481 482 483

    return False


A
alesapin 已提交
484 485 486 487 488 489 490 491
class BuildFlags(object):
    THREAD = 'thread-sanitizer'
    ADDRESS = 'address-sanitizer'
    UNDEFINED = 'ub-sanitizer'
    MEMORY = 'memory-sanitizer'
    DEBUG = 'debug-build'
    UNBUNDLED = 'unbundled-build'
    RELEASE = 'release-build'
492
    DATABASE_ORDINARY = 'database-ordinary'
A
alesapin 已提交
493 494 495 496 497
    POLYMORPHIC_PARTS = 'polymorphic-parts'


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

    if clickhouse_proc.returncode == 0:
A
Azat Khuzhin 已提交
502
        if b'-fsanitize=thread' in stdout:
A
alesapin 已提交
503
            result.append(BuildFlags.THREAD)
A
Azat Khuzhin 已提交
504
        elif b'-fsanitize=address' in stdout:
A
alesapin 已提交
505
            result.append(BuildFlags.ADDRESS)
A
Azat Khuzhin 已提交
506
        elif b'-fsanitize=undefined' in stdout:
A
alesapin 已提交
507
            result.append(BuildFlags.UNDEFINED)
A
Azat Khuzhin 已提交
508
        elif b'-fsanitize=memory' in stdout:
A
alesapin 已提交
509 510
            result.append(BuildFlags.MEMORY)
    else:
M
typo  
myrrc 已提交
511
        raise Exception("Cannot get information about build from server errorcode {}, stderr {}".format(clickhouse_proc.returncode, stderr))
A
alesapin 已提交
512 513

    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 = 'BUILD_TYPE'")
A
alesapin 已提交
515 516

    if clickhouse_proc.returncode == 0:
A
Azat Khuzhin 已提交
517
        if b'Debug' in stdout:
A
alesapin 已提交
518
            result.append(BuildFlags.DEBUG)
A
Azat Khuzhin 已提交
519
        elif b'RelWithDebInfo' in stdout or b'Release' in stdout:
A
alesapin 已提交
520 521
            result.append(BuildFlags.RELEASE)
    else:
M
typo  
myrrc 已提交
522
        raise Exception("Cannot get information about build from server errorcode {}, stderr {}".format(clickhouse_proc.returncode, stderr))
A
alesapin 已提交
523 524

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

    if clickhouse_proc.returncode == 0:
A
Azat Khuzhin 已提交
528
        if b'ON' in stdout or b'1' in stdout:
A
alesapin 已提交
529 530
            result.append(BuildFlags.UNBUNDLED)
    else:
M
typo  
myrrc 已提交
531
        raise Exception("Cannot get information about build from server errorcode {}, stderr {}".format(clickhouse_proc.returncode, stderr))
A
alesapin 已提交
532 533

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

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

A
Anton Popov 已提交
542 543 544 545 546 547 548 549 550
    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 已提交
551 552 553
    return result


P
proller 已提交
554 555
def main(args):
    global SERVER_DIED
A
Alexey Milovidov 已提交
556
    global stop_time
P
proller 已提交
557 558
    global exit_code
    global server_logs_level
A
alesapin 已提交
559

560
    def is_data_present():
P
proller 已提交
561
        clickhouse_proc = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
A
Azat Khuzhin 已提交
562
        (stdout, stderr) = clickhouse_proc.communicate(b"EXISTS TABLE test.hits")
P
proller 已提交
563
        if clickhouse_proc.returncode != 0:
P
proller 已提交
564
            raise CalledProcessError(clickhouse_proc.returncode, args.client, stderr)
565

A
Azat Khuzhin 已提交
566
        return stdout.startswith(b'1')
567

568
    if not check_server_started(args.client, args.server_check_retries):
M
typo  
myrrc 已提交
569 570 571 572
        raise Exception(
            "Server is not responding. Cannot execute 'SELECT 1' query. \
            Note: if you are using unbundled mode, you also have to specify -c option.")

A
alesapin 已提交
573
    build_flags = collect_build_flags(args.client)
574 575
    if args.antlr:
        build_flags.append('antlr')
M
typo  
myrrc 已提交
576

A
alesapin 已提交
577 578 579
    if args.use_skip_list:
        tests_to_skip_from_list = collect_tests_to_skip(args.skip_list_path, build_flags)
    else:
A
Fixes  
alesapin 已提交
580
        tests_to_skip_from_list = set([])
A
alesapin 已提交
581

A
alesapin 已提交
582 583 584 585
    if args.skip:
        args.skip = set(args.skip) | tests_to_skip_from_list
    else:
        args.skip = tests_to_skip_from_list
586

587 588 589
    if args.use_skip_list and not args.sequential:
        args.sequential = collect_sequential_list(args.skip_list_path)

P
proller 已提交
590 591 592
    base_dir = os.path.abspath(args.queries)
    tmp_dir = os.path.abspath(args.tmp)

593
    # Keep same default values as in queries/shell_config.sh
P
proller 已提交
594
    os.environ.setdefault("CLICKHOUSE_BINARY", args.binary)
595
    #os.environ.setdefault("CLICKHOUSE_CLIENT", args.client)
596
    os.environ.setdefault("CLICKHOUSE_CONFIG", args.configserver)
P
proller 已提交
597 598
    if args.configclient:
        os.environ.setdefault("CLICKHOUSE_CONFIG_CLIENT", args.configclient)
P
proller 已提交
599
    os.environ.setdefault("CLICKHOUSE_TMP", tmp_dir)
600

601
    # Force to print server warnings in stderr
602
    # Shell scripts could change logging level
603 604
    os.environ.setdefault("CLICKHOUSE_CLIENT_SERVER_LOGS_LEVEL", server_logs_level)

A
Alexey Milovidov 已提交
605 606 607 608
    # This code is bad as the time is not monotonic
    if args.global_time_limit:
        stop_time = time() + args.global_time_limit

609
    if args.zookeeper is None:
A
Azat Khuzhin 已提交
610
        code, out = subprocess.getstatusoutput(args.extract_from_config + " --try --config " + args.configserver + ' --key zookeeper | grep . | wc -l')
611 612 613 614 615 616
        try:
            if int(out) > 0:
                args.zookeeper = True
            else:
                args.zookeeper = False
        except ValueError:
617 618 619
            args.zookeeper = False

    if args.shard is None:
A
Azat Khuzhin 已提交
620
        code, out = subprocess.getstatusoutput(args.extract_from_config + " --try --config " + args.configserver + ' --key listen_host | grep -E "127.0.0.2|::"')
P
proller 已提交
621
        if out:
P
proller 已提交
622
            args.shard = True
P
proller 已提交
623 624
        else:
            args.shard = False
625

626
    if args.database and args.database != "test":
A
Azat Khuzhin 已提交
627 628
        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)))
629

A
Azat Khuzhin 已提交
630 631
    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)))
632 633 634 635 636

    def is_test_from_dir(suite_dir, case):
        case_file = os.path.join(suite_dir, case)
        (name, ext) = os.path.splitext(case)
        return os.path.isfile(case_file) and (ext == '.sql' or ext == '.sh' or ext == '.py')
P
proller 已提交
637

638 639 640 641 642
    def sute_key_func(item):
       if args.order == 'random':
             return random()

       if -1 == item.find('_'):
A
Azat Khuzhin 已提交
643
           return 99998, ''
644 645 646 647 648 649

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

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

652
    total_tests_run = 0
653
    for suite in sorted(os.listdir(base_dir), key=sute_key_func):
654 655 656 657 658 659 660
        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
661 662 663 664 665

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

666 667 668
        suite = suite_re_obj.group(1)
        if os.path.isdir(suite_dir):

P
proller 已提交
669
            if 'stateful' in suite and not args.no_stateful and not is_data_present():
670
                print("Won't run stateful tests because test data wasn't loaded.")
671
                continue
672 673 674
            if 'stateless' in suite and args.no_stateless:
                print("Won't run stateless tests because they were manually disabled.")
                continue
675 676 677
            if 'stateful' in suite and args.no_stateful:
                print("Won't run stateful tests because they were manually disabled.")
                continue
678

P
proller 已提交
679 680 681
            # Reverse sort order: we want run newest test first.
            # And not reverse subtests
            def key_func(item):
682
                if args.order == 'random':
683
                    return random()
684 685 686 687 688 689

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

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

P
proller 已提交
690
                prefix, suffix = item.split('_', 1)
691 692

                try:
A
Alexey Milovidov 已提交
693
                    return reverse * int(prefix)
694 695 696
                except ValueError:
                    return 99997

697
            all_tests = os.listdir(suite_dir)
A
Azat Khuzhin 已提交
698
            all_tests = [case for case in all_tests if is_test_from_dir(suite_dir, case)]
699 700
            if args.test:
                all_tests = [t for t in all_tests if any([re.search(r, t) for r in args.test])]
701
            all_tests.sort(key=key_func)
702

A
alesapin 已提交
703
            jobs = args.jobs
704 705 706 707 708 709 710 711
            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 已提交
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
            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 已提交
730

731
                with closing(multiprocessing.Pool(processes=jobs)) as pool:
732
                    pool.map(run_tests_array, parallel_tests_array)
733

A
alesapin 已提交
734
                run_tests_array((sequential_tests, suite, suite_dir, suite_tmp_dir))
735 736
                total_tests_run += len(sequential_tests) + len(parallel_tests)
            else:
A
alesapin 已提交
737
                run_tests_array((all_tests, suite, suite_dir, suite_tmp_dir))
738
                total_tests_run += len(all_tests)
739

740
    if args.hung_check:
741 742 743 744 745 746 747 748

        # 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)

749 750 751 752 753 754 755 756
        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))
757 758 759 760 761

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

762 763
                print("\nCollecting stacktraces from all running threads with gdb:")
                print(get_stacktraces_from_gdb(server_pid))
764
            else:
765 766 767 768 769 770 771
                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
772
        else:
773
            print(colored("\nNo queries hung.", args, "green", attrs=["bold"]))
774

775 776 777 778
    if total_tests_run == 0:
        print("No tests were run.")
        sys.exit(1)

779 780
    sys.exit(exit_code)

781

A
alesapin 已提交
782
def find_binary(name):
783 784
    if os.path.exists(name) and os.access(name, os.X_OK):
        return True
A
alesapin 已提交
785 786 787 788 789 790
    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 已提交
791 792 793 794 795 796
    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

797

798
def get_additional_client_options(args):
799 800 801 802 803 804 805 806 807 808 809
    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 ''
810 811


A
alesapin 已提交
812 813 814 815 816 817
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:
818 819 820
        content = skip_list_file.read()
        # allows to have comments in skip_list.json
        skip_dict = json.loads(json_minify(content))
A
alesapin 已提交
821 822
        for build_flag in build_flags:
            result |= set(skip_dict[build_flag])
A
alesapin 已提交
823 824 825 826

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

A
alesapin 已提交
827 828
    return result

829 830 831 832 833 834 835 836 837

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 已提交
838 839
        if 'parallel' in skip_dict:
            return skip_dict['parallel']
840 841 842
        return set([])


843
if __name__ == '__main__':
844 845 846
    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 已提交
847 848 849 850 851 852 853

    parser.add_argument('-b', '--binary', default='clickhouse',
        help='Path to clickhouse (if bundled, clickhouse-server otherwise) binary or name of binary in PATH')

    parser.add_argument('-c', '--client',
        help='Path to clickhouse-client (if unbundled, useless otherwise) binary of name of binary in PATH')

854 855 856 857 858
    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 已提交
859
    parser.add_argument('--global_time_limit', type=int, help='Stop if executing more than specified time (after current test finished)')
860
    parser.add_argument('test', nargs='*', help='Optional test case name regex')
861 862
    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')
863
    parser.add_argument('--order', default='desc', choices=['asc', 'desc', 'random'], help='Run order')
864 865
    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 已提交
866
    parser.add_argument('--force-color', action='store_true', default=False)
867
    parser.add_argument('--database', help='Database for tests (random name test_XXXXXX by default)')
P
proller 已提交
868
    parser.add_argument('--parallel', default='1/1', help='One parallel test run number/total')
P
proller 已提交
869
    parser.add_argument('-j', '--jobs', default=1, nargs='?', type=int, help='Run all tests in parallel')
870
    parser.add_argument('-U', '--unified', default=3, type=int, help='output NUM lines of unified context')
871
    parser.add_argument('-r', '--server-check-retries', default=30, type=int, help='Num of tries to execute SELECT 1 before tests started')
A
alesapin 已提交
872 873
    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")
874
    parser.add_argument('--db-engine', help='Database engine name')
875 876

    parser.add_argument('--no-stateless', action='store_true', help='Disable all stateless tests')
877
    parser.add_argument('--no-stateful', action='store_true', help='Disable all stateful tests')
878
    parser.add_argument('--skip', nargs='+', help="Skip these tests")
879
    parser.add_argument('--sequential', nargs='+', help="Run these tests sequentially even if --parallel specified")
880
    parser.add_argument('--no-long', action='store_false', dest='no_long', help='Do not run long tests')
881
    parser.add_argument('--client-option', nargs='+', help='Specify additional client argument')
882
    parser.add_argument('--print-time', action='store_true', dest='print_time', help='Print test time')
883 884 885 886 887 888
    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')
I
Ivan 已提交
889
    parser.add_argument('--antlr', action='store_true', default=False, dest='antlr', help='Use new ANTLR parser in tests')
890 891

    args = parser.parse_args()
892

A
Alexander Kuzmenkov 已提交
893
    if args.queries and not os.path.isdir(args.queries):
A
Alexander Kuzmenkov 已提交
894
        print("Cannot access the specified directory with queries (" + args.queries + ")", file=sys.stderr)
A
Alexander Kuzmenkov 已提交
895 896 897 898
        exit(1)

    # Autodetect the directory with queries if not specified
    if args.queries is None:
899
        args.queries = 'queries'
A
Alexander Kuzmenkov 已提交
900 901 902 903 904 905 906

    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.
907 908
        if args.tmp is None:
            args.tmp = '/tmp/clickhouse-test'
A
Alexander Kuzmenkov 已提交
909 910 911 912 913 914 915

        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):
916
        print("Failed to detect path to the queries directory. Please specify it with '--queries' option.", file=sys.stderr)
A
akuzm 已提交
917
        exit(1)
A
alesapin 已提交
918

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

A
alesapin 已提交
921 922
    if args.skip_list_path is None:
        args.skip_list_path = os.path.join(args.queries, 'skip_list.json')
A
alesapin 已提交
923

924 925 926
    if args.sequential is None:
        args.sequential = set([])

P
proller 已提交
927 928
    if args.tmp is None:
        args.tmp = args.queries
929
    if args.client is None:
A
alesapin 已提交
930
        if find_binary(args.binary + '-client'):
P
proller 已提交
931
            args.client = args.binary + '-client'
M
typo  
myrrc 已提交
932 933

            print("Using " + args.client + " as client program (expecting unbundled mode)")
A
alesapin 已提交
934
        elif find_binary(args.binary):
P
proller 已提交
935
            args.client = args.binary + ' client'
M
typo  
myrrc 已提交
936 937

            print("Using " + args.client + " as client program (expecting bundled mode)")
938
        else:
M
typo  
myrrc 已提交
939
            print("No 'clickhouse' or 'clickhouse-client' client binary found", file=sys.stderr)
A
alesapin 已提交
940 941 942
            parser.print_help()
            exit(1)

P
proller 已提交
943
        if args.configclient:
944 945 946 947 948
            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")
949 950 951
        if os.getenv("CLICKHOUSE_DATABASE"):
            args.client += ' --database=' + os.getenv("CLICKHOUSE_DATABASE")

952
    if args.client_option:
953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968
        # 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)

969 970 971
    if args.antlr:
        os.environ['CLICKHOUSE_CLIENT_OPT'] += ' --use_antlr_parser=1'

P
proller 已提交
972 973 974 975 976 977
    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 已提交
978
    if args.jobs is None:
979
        args.jobs = multiprocessing.cpu_count()
P
proller 已提交
980

981
    main(args)