clickhouse-test 20.0 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
#!/usr/bin/env python
import sys
import os
import os.path
import re
import lxml.etree as et

from argparse import ArgumentParser
from argparse import FileType
from pprint import pprint
11
import shlex
12
import subprocess
13 14 15 16
from subprocess import check_call
from subprocess import Popen
from subprocess import PIPE
from subprocess import CalledProcessError
17 18 19
from datetime import datetime
from time import sleep
from errno import ESRCH
20
from termcolor import colored
21
from random import random
P
proller 已提交
22
import commands
23 24


25 26 27 28 29 30 31
OP_SQUARE_BRACKET = colored("[", attrs=['bold'])
CL_SQUARE_BRACKET = colored("]", attrs=['bold'])

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

33

34 35 36 37 38 39 40 41 42 43 44 45 46
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:
            return unichr(int(s, base))
        return default
    s = re.sub(ur"&#(\d+);?", lambda c: str_to_int(c.group(1), c.group(0)), s)
    s = re.sub(ur"&#[xX]([0-9a-fA-F]+);?", lambda c: str_to_int(c.group(1), c.group(0), base=16), s)
    s = re.sub(ur"[\x00-\x08\x0b\x0e-\x1f\x7f]", "", s)
    return s

47 48
def main(args):

49 50 51
    SERVER_DIED = False

    def is_data_present():
P
proller 已提交
52 53
        clickhouse_proc = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
        (stdout, stderr) = clickhouse_proc.communicate("EXISTS TABLE test.hits")
P
proller 已提交
54
        if clickhouse_proc.returncode != 0:
P
proller 已提交
55
            raise CalledProcessError(clickhouse_proc.returncode, args.client, stderr)
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

        return stdout.startswith('1')


    def dump_report(destination, suite, test_case, report):
        if destination is not None:
            destination_file = os.path.join(destination, suite, test_case + ".xml")
            destination_dir = os.path.dirname(destination_file)
            if not os.path.exists(destination_dir):
                os.makedirs(destination_dir)
            with open(destination_file, 'w') as report_file:
                report_root = et.Element("testsuites", attrib = {'name': 'ClickHouse Tests'})
                report_suite = et.Element("testsuite", attrib = {"name": suite})
                report_suite.append(report)
                report_root.append(report_suite)
                report_file.write(et.tostring(report_root, encoding = "UTF-8", xml_declaration=True, pretty_print=True))

P
proller 已提交
73 74 75
    base_dir = os.path.abspath(args.queries)
    tmp_dir = os.path.abspath(args.tmp)

76
    # Keep same default values as in queries/shell_config.sh
P
proller 已提交
77 78
    os.environ.setdefault("CLICKHOUSE_BINARY", args.binary)
    os.environ.setdefault("CLICKHOUSE_CLIENT", args.client)
79
    os.environ.setdefault("CLICKHOUSE_CONFIG", args.configserver)
P
proller 已提交
80 81
    if args.configclient:
        os.environ.setdefault("CLICKHOUSE_CONFIG_CLIENT", args.configclient)
P
proller 已提交
82
    os.environ.setdefault("CLICKHOUSE_TMP", tmp_dir)
83

84
    # Force to print server warnings in stderr
85 86
    # Shell scripts could change logging level
    server_logs_level = "warning"
87 88
    os.environ.setdefault("CLICKHOUSE_CLIENT_SERVER_LOGS_LEVEL", server_logs_level)

89
    if args.zookeeper is None:
P
proller 已提交
90
        code, out = commands.getstatusoutput(args.extract_from_config +" --try --config " + args.configserver + ' --key zookeeper | grep . | wc -l')
91 92 93 94 95 96
        try:
            if int(out) > 0:
                args.zookeeper = True
            else:
                args.zookeeper = False
        except ValueError:
97 98 99
            args.zookeeper = False

    if args.shard is None:
P
proller 已提交
100
        code, out = commands.getstatusoutput(args.extract_from_config + " --try --config " + args.configserver + ' --key listen_host | grep -E "127.0.0.2|::"')
P
proller 已提交
101
        if out:
P
proller 已提交
102
            args.shard = True
P
proller 已提交
103 104
        else:
            args.shard = False
105

106 107
    passed_total = 0
    skipped_total = 0
108 109
    failures_total = 0

P
proller 已提交
110 111 112
    clickhouse_proc_create = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
    clickhouse_proc_create.communicate("CREATE DATABASE IF NOT EXISTS test")

113 114 115 116 117 118 119 120
    for suite in sorted(os.listdir(base_dir)):
        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
121 122 123 124 125

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

126 127 128 129 130
        suite = suite_re_obj.group(1)
        if os.path.isdir(suite_dir):
            print("\nRunning {} tests.\n".format(suite))

            failures = 0
131
            failures_chain = 0
132
            if 'stateful' in suite and not is_data_present():
133
                print("Won't run stateful tests because test data wasn't loaded.")
134 135
                continue

P
proller 已提交
136 137 138
            # Reverse sort order: we want run newest test first.
            # And not reverse subtests
            def key_func(item):
139
                if args.order == 'random':
140
                    return random()
141 142 143 144 145 146

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

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

P
proller 已提交
147
                prefix, suffix = item.split('_', 1)
148 149 150 151 152 153

                try:
                    return reverse * int(prefix), suffix
                except ValueError:
                    return 99997

P
proller 已提交
154
            for case in sorted(filter(lambda case: re.search(args.test, case) if args.test else True, os.listdir(suite_dir)), key=key_func):
155 156 157 158
                if SERVER_DIED:
                    break

                case_file = os.path.join(suite_dir, case)
159 160 161
                (name, ext) = os.path.splitext(case)

                if os.path.isfile(case_file) and (ext == '.sql' or ext == '.sh' or ext == '.py'):
162 163
                    report_testcase = et.Element("testcase", attrib = {"name": name})

E
Evgeny Konkov 已提交
164
                    try:
A
Alexey Milovidov 已提交
165
                        print "{0:72}".format(name + ": "),
E
Evgeny Konkov 已提交
166
                        sys.stdout.flush()
A
Alexey Milovidov 已提交
167

P
proller 已提交
168 169 170 171 172
                        if args.skip and any(s in name for s in args.skip):
                            report_testcase.append(et.Element("skipped", attrib = {"message": "skip"}))
                            print(MSG_SKIPPED + " - skip")
                            skipped_total += 1
                        elif not args.zookeeper and 'zookeeper' in name:
E
Evgeny Konkov 已提交
173 174
                            report_testcase.append(et.Element("skipped", attrib = {"message": "no zookeeper"}))
                            print(MSG_SKIPPED + " - no zookeeper")
175
                            skipped_total += 1
E
Evgeny Konkov 已提交
176 177 178
                        elif not args.shard and 'shard' in name:
                            report_testcase.append(et.Element("skipped", attrib = {"message": "no shard"}))
                            print(MSG_SKIPPED + " - no shard")
179
                            skipped_total += 1
P
proller 已提交
180 181 182 183
                        elif not args.no_long and 'long' in name:
                            report_testcase.append(et.Element("skipped", attrib = {"message": "no long"}))
                            print(MSG_SKIPPED + " - no long")
                            skipped_total += 1
184
                        else:
185
                            disabled_file = os.path.join(suite_dir, name) + '.disabled'
186

187 188 189 190
                            if os.path.exists(disabled_file) and not args.disabled:
                                message = open(disabled_file, 'r').read()
                                report_testcase.append(et.Element("skipped", attrib = {"message": message}))
                                print(MSG_SKIPPED + " - " + message)
E
Evgeny Konkov 已提交
191
                            else:
P
proller 已提交
192 193 194 195 196

                                if args.testname:
                                    clickhouse_proc = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
                                    clickhouse_proc.communicate("SELECT 'Running test {suite}/{case} from pid={pid}';".format(pid = os.getpid(), case = case, suite = suite))

197
                                reference_file = os.path.join(suite_dir, name) + '.reference'
198 199
                                stdout_file = os.path.join(suite_tmp_dir, name) + '.stdout'
                                stderr_file = os.path.join(suite_tmp_dir, name) + '.stderr'
A
Alexey Milovidov 已提交
200

201
                                if ext == '.sql':
A
Alexey Milovidov 已提交
202
                                    command = "{0} --send_logs_level={1} --testmode --multiquery < {2} > {3} 2> {4}".format(args.client, server_logs_level, case_file, stdout_file, stderr_file)
203
                                else:
204
                                    command = "{} > {} 2> {}".format(case_file, stdout_file, stderr_file)
A
Alexey Milovidov 已提交
205

206 207 208
                                proc = Popen(command, shell = True)
                                start_time = datetime.now()
                                while (datetime.now() - start_time).total_seconds() < args.timeout and proc.poll() is None:
P
proller 已提交
209
                                    sleep(0.01)
A
Alexey Milovidov 已提交
210

211 212 213 214 215 216
                                if proc.returncode is None:
                                    try:
                                        proc.kill()
                                    except OSError as e:
                                        if e.errno != ESRCH:
                                            raise
A
Alexey Milovidov 已提交
217

218
                                    failure = et.Element("failure", attrib = {"message": "Timeout"})
E
Evgeny Konkov 已提交
219
                                    report_testcase.append(failure)
A
Alexey Milovidov 已提交
220

221
                                    failures += 1
222 223 224 225 226 227
                                    print("{0} - Timeout!".format(MSG_FAIL))
                                else:
                                    stdout = open(stdout_file, 'r').read() if os.path.exists(stdout_file) else ''
                                    stdout = unicode(stdout, errors='replace', encoding='utf-8')
                                    stderr = open(stderr_file, 'r').read() if os.path.exists(stderr_file) else ''
                                    stderr = unicode(stderr, errors='replace', encoding='utf-8')
A
Alexey Milovidov 已提交
228

229 230 231
                                    if proc.returncode != 0:
                                        failure = et.Element("failure", attrib = {"message": "return code {}".format(proc.returncode)})
                                        report_testcase.append(failure)
A
Alexey Milovidov 已提交
232

233 234 235
                                        stdout_element = et.Element("system-out")
                                        stdout_element.text = et.CDATA(stdout)
                                        report_testcase.append(stdout_element)
A
Alexey Milovidov 已提交
236

237 238
                                        failures += 1
                                        failures_chain += 1
239
                                        print("{0} - return code {1}".format(MSG_FAIL, proc.returncode))
A
Alexey Milovidov 已提交
240

241 242 243 244
                                        if stderr:
                                            stderr_element = et.Element("system-err")
                                            stderr_element.text = et.CDATA(stderr)
                                            report_testcase.append(stderr_element)
P
proller 已提交
245
                                            print(stderr.encode('utf-8'))
A
Alexey Milovidov 已提交
246

247 248
                                        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
A
Alexey Milovidov 已提交
249

250 251 252
                                    elif stderr:
                                        failure = et.Element("failure", attrib = {"message": "having stderror"})
                                        report_testcase.append(failure)
A
Alexey Milovidov 已提交
253

254 255 256
                                        stderr_element = et.Element("system-err")
                                        stderr_element.text = et.CDATA(stderr)
                                        report_testcase.append(stderr_element)
A
Alexey Milovidov 已提交
257

258 259
                                        failures += 1
                                        failures_chain += 1
260 261 262
                                        print("{0} - having stderror:\n{1}".format(MSG_FAIL, stderr.encode('utf-8')))
                                    elif 'Exception' in stdout:
                                        failure = et.Element("error", attrib = {"message": "having exception"})
E
Evgeny Konkov 已提交
263
                                        report_testcase.append(failure)
A
Alexey Milovidov 已提交
264

E
Evgeny Konkov 已提交
265
                                        stdout_element = et.Element("system-out")
266
                                        stdout_element.text = et.CDATA(stdout)
E
Evgeny Konkov 已提交
267
                                        report_testcase.append(stdout_element)
268

269 270
                                        failures += 1
                                        failures_chain += 1
271 272 273 274 275
                                        print("{0} - having exception:\n{1}".format(MSG_FAIL, stdout.encode('utf-8')))
                                    elif not os.path.isfile(reference_file):
                                        skipped = et.Element("skipped", attrib = {"message": "no reference file"})
                                        report_testcase.append(skipped)
                                        print("{0} - no reference file".format(MSG_UNKNOWN))
E
Evgeny Konkov 已提交
276
                                    else:
277 278 279
                                        result_is_different = subprocess.call(['cmp', '-s', reference_file, stdout_file], stdout = PIPE)

                                        if result_is_different:
A
alexey-milovidov 已提交
280
                                            (diff, _) = Popen(['diff', '--unified', reference_file, stdout_file], stdout = PIPE).communicate()
281 282 283 284 285 286 287 288 289 290 291 292
                                            diff = unicode(diff, errors='replace', encoding='utf-8')

                                            failure = et.Element("failure", attrib = {"message": "result differs with reference"})
                                            report_testcase.append(failure)

                                            stdout_element = et.Element("system-out")
                                            try:
                                                stdout_element.text = et.CDATA(diff)
                                            except:
                                                stdout_element.text = et.CDATA(remove_control_characters(diff))

                                            report_testcase.append(stdout_element)
293
                                            failures += 1
294 295
                                            print("{0} - result differs with reference:\n{1}".format(MSG_FAIL, diff.encode('utf-8')))
                                        else:
296 297
                                            passed_total += 1
                                            failures_chain = 0
298 299 300 301 302
                                            print(MSG_OK)
                                            if os.path.exists(stdout_file):
                                                os.remove(stdout_file)
                                            if os.path.exists(stderr_file):
                                                os.remove(stderr_file)
303 304 305
                    except KeyboardInterrupt as e:
                        print(colored("Break tests execution", "red"))
                        raise e
E
Evgeny Konkov 已提交
306 307 308 309 310
                    except:
                        (exc_type, exc_value) = sys.exc_info()[:2]
                        error = et.Element("error", attrib = {"type": exc_type.__name__, "message": str(exc_value)})
                        report_testcase.append(error)

311
                        failures += 1
E
Evgeny Konkov 已提交
312 313 314
                        print("{0} - Test internal error: {1}\n{2}".format(MSG_FAIL, exc_type.__name__, exc_value))
                    finally:
                        dump_report(args.output, suite, name, report_testcase)
315

316 317 318
                    if failures_chain >= 20:
                        break

319 320 321
            failures_total = failures_total + failures

    if failures_total > 0:
322
        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), "red", attrs=["bold"]))
323 324
        sys.exit(1)
    else:
325
        print(colored("\n{passed_total} tests passed. {skipped_total} tests skipped.".format(passed_total = passed_total, skipped_total = skipped_total), "green", attrs=["bold"]))
326
        sys.exit(0)
327

328 329

if __name__ == '__main__':
330
    parser = ArgumentParser(description = 'ClickHouse functional tests')
331 332
    parser.add_argument('-q', '--queries', help = 'Path to queries dir')
    parser.add_argument('--tmp', help = 'Path to tmp dir')
333 334
    parser.add_argument('-b', '--binary', default = 'clickhouse', help = 'Main clickhouse binary')
    parser.add_argument('-c', '--client', help = 'Client program')
P
proller 已提交
335
    parser.add_argument('--extract_from_config', help = 'extract-from-config program')
P
proller 已提交
336 337
    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')
338 339 340
    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')
    parser.add_argument('test', nargs = '?', help = 'Optional test case name regex')
341
    parser.add_argument('-d', '--disabled', action = 'store_true', default = False, help = 'Also run disabled tests')
342
    parser.add_argument('--stop', action = 'store_true', default = None, dest = 'stop', help = 'Stop on network errors')
343
    parser.add_argument('--order', default = 'desc', help = 'Run order (asc, desc, random)')
P
proller 已提交
344
    parser.add_argument('--testname', action = 'store_true', default = None, dest = 'testname', help = 'Make query with test name before test run')
345

P
proller 已提交
346
    parser.add_argument('--skip', nargs='+', help = "Skip these tests")
P
proller 已提交
347
    parser.add_argument('--no-long', action = 'store_false', dest = 'no_long', help = 'Do not run long tests')
348 349 350
    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')
P
proller 已提交
351
    group = parser.add_mutually_exclusive_group(required = False)
352 353 354 355
    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')

    args = parser.parse_args()
356 357 358

    if args.queries is None and os.path.isdir('queries'):
        args.queries = 'queries'
P
proller 已提交
359
    elif args.queries is None:
P
proller 已提交
360 361 362 363
        if (os.path.isdir('/usr/local/share/clickhouse-test/queries')):
            args.queries = '/usr/local/share/clickhouse-test/queries'
        if (args.queries is None and os.path.isdir('/usr/share/clickhouse-test/queries')):
            args.queries = '/usr/share/clickhouse-test/queries'
364 365
        if args.tmp is None:
            args.tmp = '/tmp/clickhouse-test'
P
proller 已提交
366 367
    if args.tmp is None:
        args.tmp = args.queries
368

369
    if args.client is None:
P
proller 已提交
370 371 372 373
        if os.access(args.binary + '-client', os.X_OK):
            args.client = args.binary + '-client'
        else:
            args.client = args.binary + ' client'
P
proller 已提交
374
        if args.configclient:
375 376 377 378 379 380
            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")

P
proller 已提交
381 382 383 384 385 386
    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'

387
    main(args)