clickhouse-test 14.8 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
22 23


24 25 26 27 28 29 30
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
31

32

33 34 35 36 37 38 39 40 41 42 43 44 45
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

46 47
def main(args):

48 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")
54
        if 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 73 74 75

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


    if args.zookeeper is None:
        try:
P
proller 已提交
76
            check_call(['grep', '-q', '<zookeeper', '/etc/clickhouse-server/config-preprocessed.xml'])
77 78 79 80 81 82
            args.zookeeper = True
        except CalledProcessError:
            args.zookeeper = False

    if args.shard is None:
        try:
P
proller 已提交
83
            check_call(['grep', '-qE', '"127.0.0.2|<listen_host>::</listen_host>"', '/etc/clickhouse-server/config-preprocessed.xml'])
84 85
            args.shard = True
        except CalledProcessError:
P
proller 已提交
86
            args.shard = False
87 88 89 90 91

    base_dir = os.path.abspath(args.queries)

    failures_total = 0

92 93
    # Keep same default values as in queries/0_stateless/00000_sh_lib.sh
    os.environ.setdefault("CLICKHOUSE_BINARY", args.binary)
94 95
    os.environ.setdefault("CLICKHOUSE_CLIENT", args.client)

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
    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
        suite = suite_re_obj.group(1)
        if os.path.isdir(suite_dir):
            print("\nRunning {} tests.\n".format(suite))

            failures = 0
            if 'stateful' in suite and not is_data_present():
                print("Won't run stateful tests because test data wasn't loaded. See README.txt.")
                continue

P
proller 已提交
113 114 115
            # Reverse sort order: we want run newest test first.
            # And not reverse subtests
            def key_func(item):
116 117
                if args.random:
                    return random()
P
proller 已提交
118 119 120
                prefix, suffix = item.split('_', 1)
                return -int(prefix), suffix
            for case in sorted(filter(lambda case: re.search(args.test, case) if args.test else True, os.listdir(suite_dir)), key=key_func):
121 122 123 124 125 126 127 128
                if SERVER_DIED:
                    break

                case_file = os.path.join(suite_dir, case)
                if os.path.isfile(case_file) and (case.endswith('.sh') or case.endswith('.py') or case.endswith('.sql')):
                    (name, ext) = os.path.splitext(case)
                    report_testcase = et.Element("testcase", attrib = {"name": name})

E
Evgeny Konkov 已提交
129
                    try:
P
proller 已提交
130 131 132 133
                        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))

E
Evgeny Konkov 已提交
134 135
                        print "{0:70}".format(name + ": "),
                        sys.stdout.flush()
A
Alexey Milovidov 已提交
136

E
Evgeny Konkov 已提交
137 138 139 140 141 142
                        if not args.zookeeper and 'zookeeper' in name:
                            report_testcase.append(et.Element("skipped", attrib = {"message": "no zookeeper"}))
                            print(MSG_SKIPPED + " - no zookeeper")
                        elif not args.shard and 'shard' in name:
                            report_testcase.append(et.Element("skipped", attrib = {"message": "no shard"}))
                            print(MSG_SKIPPED + " - no shard")
143
                        else:
E
Evgeny Konkov 已提交
144 145 146
                            reference_file = os.path.join(suite_dir, name) + '.reference'
                            stdout_file = os.path.join(suite_dir, name) + '.stdout'
                            stderr_file = os.path.join(suite_dir, name) + '.stderr'
A
Alexey Milovidov 已提交
147

E
Evgeny Konkov 已提交
148 149 150 151
                            if ext == '.sql':
                                command = "{0} --multiquery < {1} > {2} 2> {3}".format(args.client, case_file, stdout_file, stderr_file)
                            else:
                                command = "{0} > {1} 2> {2}".format(case_file, stdout_file, stderr_file)
A
Alexey Milovidov 已提交
152

E
Evgeny Konkov 已提交
153 154 155 156
                            proc = Popen(command, shell = True)
                            start_time = datetime.now()
                            while (datetime.now() - start_time).total_seconds() < args.timeout and proc.poll() is None:
                                sleep(0)
A
Alexey Milovidov 已提交
157

E
Evgeny Konkov 已提交
158 159 160 161 162 163
                            if proc.returncode is None:
                                try:
                                    proc.kill()
                                except OSError as e:
                                    if e.errno != ESRCH:
                                        raise
A
Alexey Milovidov 已提交
164

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

168
                                failures = failures + 1
E
Evgeny Konkov 已提交
169 170 171 172 173 174
                                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 已提交
175

E
Evgeny Konkov 已提交
176 177 178
                                if proc.returncode != 0:
                                    failure = et.Element("failure", attrib = {"message": "return code {}".format(proc.returncode)})
                                    report_testcase.append(failure)
A
Alexey Milovidov 已提交
179

E
Evgeny Konkov 已提交
180 181 182
                                    stdout_element = et.Element("system-out")
                                    stdout_element.text = et.CDATA(stdout)
                                    report_testcase.append(stdout_element)
A
Alexey Milovidov 已提交
183

E
Evgeny Konkov 已提交
184 185
                                    failures = failures + 1
                                    print("{0} - return code {1}".format(MSG_FAIL, proc.returncode))
A
Alexey Milovidov 已提交
186

E
Evgeny Konkov 已提交
187 188 189 190 191
                                    if stderr:
                                        stderr_element = et.Element("system-err")
                                        stderr_element.text = et.CDATA(stderr)
                                        report_testcase.append(stderr_element)
                                        print(stderr)
A
Alexey Milovidov 已提交
192

E
Evgeny Konkov 已提交
193 194
                                    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 已提交
195

E
Evgeny Konkov 已提交
196 197 198
                                elif stderr:
                                    failure = et.Element("failure", attrib = {"message": "having stderror"})
                                    report_testcase.append(failure)
A
Alexey Milovidov 已提交
199

200 201 202
                                    stderr_element = et.Element("system-err")
                                    stderr_element.text = et.CDATA(stderr)
                                    report_testcase.append(stderr_element)
A
Alexey Milovidov 已提交
203

E
Evgeny Konkov 已提交
204 205 206 207
                                    failures = failures + 1
                                    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"})
208
                                    report_testcase.append(failure)
A
Alexey Milovidov 已提交
209

210
                                    stdout_element = et.Element("system-out")
E
Evgeny Konkov 已提交
211
                                    stdout_element.text = et.CDATA(stdout)
212
                                    report_testcase.append(stdout_element)
A
Alexey Milovidov 已提交
213

214
                                    failures = failures + 1
E
Evgeny Konkov 已提交
215 216 217 218 219
                                    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))
220
                                else:
E
Evgeny Konkov 已提交
221
                                    result_is_different = subprocess.call(['cmp', '-s', reference_file, stdout_file], stdout = PIPE)
A
Alexey Milovidov 已提交
222

E
Evgeny Konkov 已提交
223 224 225
                                    if result_is_different:
                                        (diff, _) = Popen(['diff', '--side-by-side', reference_file, stdout_file], stdout = PIPE).communicate()
                                        diff = unicode(diff, errors='replace', encoding='utf-8')
A
Alexey Milovidov 已提交
226

E
Evgeny Konkov 已提交
227 228
                                        failure = et.Element("failure", attrib = {"message": "result differs with reference"})
                                        report_testcase.append(failure)
A
Alexey Milovidov 已提交
229

E
Evgeny Konkov 已提交
230 231 232 233 234
                                        stdout_element = et.Element("system-out")
                                        try:
                                            stdout_element.text = et.CDATA(diff)
                                        except:
                                            stdout_element.text = et.CDATA(remove_control_characters(diff))
A
Alexey Milovidov 已提交
235

E
Evgeny Konkov 已提交
236 237 238 239 240 241 242 243 244
                                        report_testcase.append(stdout_element)
                                        failures = failures + 1
                                        print("{0} - result differs with reference:\n{1}".format(MSG_FAIL, diff.encode('utf-8')))
                                    else:
                                        print(MSG_OK)
                                        if os.path.exists(stdout_file):
                                            os.remove(stdout_file)
                                        if os.path.exists(stderr_file):
                                            os.remove(stderr_file)
245 246 247
                    except KeyboardInterrupt as e:
                        print(colored("Break tests execution", "red"))
                        raise e
E
Evgeny Konkov 已提交
248 249 250 251 252 253 254 255 256
                    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)

                        failures = failures + 1
                        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)
257 258 259 260 261 262 263 264 265

            failures_total = failures_total + failures

    if failures_total > 0:
        print(colored("\nHaving {0} errors!".format(failures_total), "red", attrs=["bold"]))
        sys.exit(1)
    else:
        print(colored("\nAll tests passed.", "green", attrs=["bold"]))
        sys.exit(0)
266

267 268

if __name__ == '__main__':
269 270
    parser = ArgumentParser(description = 'ClickHouse functional tests')
    parser.add_argument('-q', '--queries', default = 'queries', help = 'Path to queries dir')
271 272
    parser.add_argument('-b', '--binary', default = 'clickhouse', help = 'Main clickhouse binary')
    parser.add_argument('-c', '--client', help = 'Client program')
273 274 275
    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')
276 277
    parser.add_argument('--stop', action = 'store_true', default = None, dest = 'stop', help = 'Stop on network errors')
    parser.add_argument('--random', action = 'store_true', default = None, dest = 'random', help = 'Randomize tests order')
P
proller 已提交
278
    parser.add_argument('--testname', action = 'store_true', default = None, dest = 'testname', help = 'Make query with test name before test run')
279 280 281 282

    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 已提交
283
    group = parser.add_mutually_exclusive_group(required = False)
284 285 286 287
    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()
288
    if args.client is None:
P
proller 已提交
289
        args.client = args.binary + '-client'
290
    main(args)