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

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

25

A
alesapin 已提交
26
MESSAGES_TO_RETRY = [
27
    "DB::Exception: ZooKeeper session has been expired",
A
alesapin 已提交
28 29 30
    "Coordination::Exception: Connection loss",
]

31

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

A
alesapin 已提交
45 46
def run_single_test(args, ext, server_logs_level, case_file, stdout_file, stderr_file):
    if ext == '.sql':
47
        command = "{0} --send_logs_level={1} --testmode --multiquery < {2} > {3} 2> {4}".format(args.client_with_database, server_logs_level, case_file, stdout_file, stderr_file)
A
alesapin 已提交
48 49 50 51 52 53 54 55
    else:
        command = "{} > {} 2> {}".format(case_file, stdout_file, stderr_file)

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

A
alexey-milovidov 已提交
56
    # Normalize randomized database names in stdout, stderr files.
P
proller 已提交
57 58
    os.system("sed -i -e 's/{test_db}/default/g' {file}".format(test_db=args.database, file=stdout_file))
    os.system("sed -i -e 's/{test_db}/default/g' {file}".format(test_db=args.database, file=stderr_file))
59

A
alesapin 已提交
60 61 62 63 64 65 66 67 68 69
    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')

    return proc, stdout, stderr

def need_retry(stderr):
    return any(msg in stderr for msg in MESSAGES_TO_RETRY)

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
def get_processlist(client_cmd):
    try:
        return subprocess.check_output("{} --query 'SHOW PROCESSLIST FORMAT Vertical'".format(client_cmd), shell=True)
    except:
        return "" #  server seems dead

def get_stacktraces(server_pid):
    cmd = "gdb -q -ex 'set pagination off' -ex 'backtrace' -ex 'thread apply all backtrace' -ex 'detach' -ex 'quit' --pid {} 2>/dev/null".format(server_pid)
    try:
        return subprocess.check_output(cmd, shell=True)
    except Exception as ex:
        return "Error occured while receiving stack traces {}".format(str(ex))

def get_server_pid(server_tcp_port):
    cmd = "lsof -i tcp:{port} | grep '*:{port}'".format(port=server_tcp_port)
    try:
        output = subprocess.check_output(cmd, shell=True)
        if output:
            columns = output.strip().split(' ')
            return int(columns[1])
        else:
            return None # server dead
    except Exception as ex:
        return None

P
proller 已提交
95 96 97 98 99 100 101 102 103
def colored(text, args, color=None, on_color=None, attrs=None):
       if sys.stdout.isatty() or args.force_color:
           return termcolor.colored(text, color, on_color, attrs)
       else:
           return text

SERVER_DIED = False
exit_code = 0

104

P
proller 已提交
105 106 107
#def run_tests_array(all_tests, suite, suite_dir, suite_tmp_dir, run_total):
def run_tests_array(all_tests_with_params):
    all_tests, suite, suite_dir, suite_tmp_dir, run_total = all_tests_with_params
P
proller 已提交
108
    global exit_code
P
proller 已提交
109
    global SERVER_DIED
110

P
proller 已提交
111 112 113 114 115 116 117 118 119 120 121 122 123
    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
124

P
proller 已提交
125 126
    if len(all_tests):
        print("\nRunning {} {} tests.".format(len(all_tests), suite) + "\n")
A
alesapin 已提交
127

P
proller 已提交
128 129 130
    for case in all_tests:
        if SERVER_DIED:
            break
A
alesapin 已提交
131

P
proller 已提交
132 133 134 135 136 137 138 139 140 141 142 143 144 145
        case_file = os.path.join(suite_dir, case)
        (name, ext) = os.path.splitext(case)

        try:
            sys.stdout.write("{0:72}".format(name + ": "))
            if run_total == 1:
                sys.stdout.flush()

            if args.skip and any(s in name for s in args.skip):
                print(MSG_SKIPPED + " - skip")
                skipped_total += 1
            elif not args.zookeeper and 'zookeeper' in name:
                print(MSG_SKIPPED + " - no zookeeper")
                skipped_total += 1
A
akuzm 已提交
146 147 148
            elif not args.shard and ('shard' in name
                    or 'distributed' in name
                    or 'global' in name):
P
proller 已提交
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
                print(MSG_SKIPPED + " - no shard")
                skipped_total += 1
            elif not args.no_long and 'long' in name:
                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:
                        clickhouse_proc = Popen(shlex.split(args.client_with_database), 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))

                    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'

                    proc, stdout, stderr = run_single_test(args, ext, server_logs_level, case_file, stdout_file, stderr_file)
                    if proc.returncode is None:
                        try:
                            proc.kill()
                        except OSError as e:
                            if e.errno != ESRCH:
                                raise

                        failures += 1
179
                        print("{0} - Timeout!".format(MSG_FAIL))
P
proller 已提交
180 181 182 183 184 185 186 187 188 189 190 191
                    else:
                        counter = 1
                        while proc.returncode != 0 and need_retry(stderr):
                            proc, stdout, stderr = run_single_test(args, ext, server_logs_level, case_file, stdout_file, stderr_file)
                            sleep(2**counter)
                            counter += 1
                            if counter > 6:
                                break

                        if proc.returncode != 0:
                            failures += 1
                            failures_chain += 1
192
                            print("{0} - return code {1}".format(MSG_FAIL, proc.returncode))
P
proller 已提交
193 194

                            if stderr:
195
                                print(stderr.encode('utf-8'))
P
proller 已提交
196 197 198 199 200 201 202

                            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

                        elif stderr:
                            failures += 1
                            failures_chain += 1
203
                            print("{0} - having stderror:\n{1}".format(MSG_FAIL, stderr.encode('utf-8')))
P
proller 已提交
204 205 206
                        elif 'Exception' in stdout:
                            failures += 1
                            failures_chain += 1
207
                            print("{0} - having exception:\n{1}".format(MSG_FAIL, stdout.encode('utf-8')))
P
proller 已提交
208
                        elif not os.path.isfile(reference_file):
209
                            print("{0} - no reference file".format(MSG_UNKNOWN))
P
proller 已提交
210 211 212 213 214 215
                        else:
                            result_is_different = subprocess.call(['diff', '-q', reference_file, stdout_file], stdout = PIPE)

                            if result_is_different:
                                diff = Popen(['diff', '--unified', reference_file, stdout_file], stdout = PIPE).communicate()[0]
                                failures += 1
216
                                print("{0} - result differs with reference:\n{1}".format(MSG_FAIL, diff))
P
proller 已提交
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
                            else:
                                passed_total += 1
                                failures_chain = 0
                                print(MSG_OK)
                                if os.path.exists(stdout_file):
                                    os.remove(stdout_file)
                                if os.path.exists(stderr_file):
                                    os.remove(stderr_file)
        except KeyboardInterrupt as e:
            print(colored("Break tests execution", args, "red"))
            raise e
        except:
            import traceback
            exc_type, exc_value, tb = sys.exc_info()
            failures += 1
232
            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 已提交
233 234 235 236 237 238 239

        if failures_chain >= 20:
            break

    failures_total = failures_total + failures

    if failures_total > 0:
240
        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 已提交
241 242 243
        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 已提交
244

P
proller 已提交
245 246 247 248 249 250
server_logs_level = "warning"

def main(args):
    global SERVER_DIED
    global exit_code
    global server_logs_level
A
alesapin 已提交
251

252
    def is_data_present():
P
proller 已提交
253 254
        clickhouse_proc = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
        (stdout, stderr) = clickhouse_proc.communicate("EXISTS TABLE test.hits")
P
proller 已提交
255
        if clickhouse_proc.returncode != 0:
P
proller 已提交
256
            raise CalledProcessError(clickhouse_proc.returncode, args.client, stderr)
257 258 259

        return stdout.startswith('1')

P
proller 已提交
260 261 262
    base_dir = os.path.abspath(args.queries)
    tmp_dir = os.path.abspath(args.tmp)

263
    # Keep same default values as in queries/shell_config.sh
P
proller 已提交
264
    os.environ.setdefault("CLICKHOUSE_BINARY", args.binary)
P
proller 已提交
265
    #os.environ.setdefault("CLICKHOUSE_CLIENT", args.client)
266
    os.environ.setdefault("CLICKHOUSE_CONFIG", args.configserver)
P
proller 已提交
267 268
    if args.configclient:
        os.environ.setdefault("CLICKHOUSE_CONFIG_CLIENT", args.configclient)
P
proller 已提交
269
    os.environ.setdefault("CLICKHOUSE_TMP", tmp_dir)
270
    os.environ.setdefault("CLICKHOUSE_DATABASE", args.database)
271

272
    # Force to print server warnings in stderr
273
    # Shell scripts could change logging level
274 275
    os.environ.setdefault("CLICKHOUSE_CLIENT_SERVER_LOGS_LEVEL", server_logs_level)

276
    if args.zookeeper is None:
P
proller 已提交
277
        code, out = commands.getstatusoutput(args.extract_from_config +" --try --config " + args.configserver + ' --key zookeeper | grep . | wc -l')
278 279 280 281 282 283
        try:
            if int(out) > 0:
                args.zookeeper = True
            else:
                args.zookeeper = False
        except ValueError:
284 285 286
            args.zookeeper = False

    if args.shard is None:
P
proller 已提交
287
        code, out = commands.getstatusoutput(args.extract_from_config + " --try --config " + args.configserver + ' --key listen_host | grep -E "127.0.0.2|::"')
P
proller 已提交
288
        if out:
P
proller 已提交
289
            args.shard = True
P
proller 已提交
290 291
        else:
            args.shard = False
292

P
proller 已提交
293
    clickhouse_proc_create = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
294
    clickhouse_proc_create.communicate("CREATE DATABASE IF NOT EXISTS " + args.database)
295 296 297
    if args.database != "test":
        clickhouse_proc_create = Popen(shlex.split(args.client), stdin=PIPE, stdout=PIPE, stderr=PIPE)
        clickhouse_proc_create.communicate("CREATE DATABASE IF NOT EXISTS test")
298 299 300 301 302

    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 已提交
303

304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
    def sute_key_func(item):
       if args.order == 'random':
             return random()

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

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

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

    for suite in sorted(os.listdir(base_dir), key=sute_key_func):
319 320 321 322 323 324 325
        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
326 327 328 329 330

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

331 332 333
        suite = suite_re_obj.group(1)
        if os.path.isdir(suite_dir):

P
proller 已提交
334
            if 'stateful' in suite and not args.no_stateful and not is_data_present():
335
                print("Won't run stateful tests because test data wasn't loaded.")
336
                continue
337 338 339
            if 'stateless' in suite and args.no_stateless:
                print("Won't run stateless tests because they were manually disabled.")
                continue
340 341 342
            if 'stateful' in suite and args.no_stateful:
                print("Won't run stateful tests because they were manually disabled.")
                continue
343

P
proller 已提交
344 345 346
            # Reverse sort order: we want run newest test first.
            # And not reverse subtests
            def key_func(item):
347
                if args.order == 'random':
348
                    return random()
349 350 351 352 353 354

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

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

P
proller 已提交
355
                prefix, suffix = item.split('_', 1)
356 357 358 359 360 361

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

362 363 364 365
            all_tests = os.listdir(suite_dir)
            all_tests = filter(lambda case: is_test_from_dir(suite_dir, case), all_tests)
            all_tests = sorted(filter(lambda case: re.search(args.test, case) if args.test else True, all_tests), key=key_func)

P
proller 已提交
366 367 368 369 370 371 372 373
            run_n, run_total = args.parallel.split('/')
            run_n = float(run_n)
            run_total = float(run_total)
            tests_n = len(all_tests)
            if run_total > tests_n:
                run_total = tests_n
            if run_n > run_total:
                continue
374

P
proller 已提交
375
            jobs = args.jobs
P
proller 已提交
376 377
            if jobs > tests_n:
                jobs = tests_n
P
proller 已提交
378 379
            if jobs > run_total:
                run_total = jobs
P
proller 已提交
380

P
proller 已提交
381 382 383 384 385
            all_tests_array = []
            for n in range(1, 1 + int(run_total)):
                start = int(tests_n / run_total * (n - 1))
                end = int(tests_n / run_total * n)
                all_tests_array.append([all_tests[start : end], suite, suite_dir, suite_tmp_dir, run_total])
P
proller 已提交
386

P
proller 已提交
387
            if jobs > 1:
P
proller 已提交
388
                with closing(multiprocessing.Pool(processes=jobs)) as pool:
P
proller 已提交
389 390 391 392
                    pool.map(run_tests_array, all_tests_array)
                    pool.terminate()
            else:
                run_tests_array(all_tests_array[int(run_n)-1])
393 394

    if args.hung_check:
395
        processlist = get_processlist(args.client_with_database)
396 397
        if processlist:
            server_pid = get_server_pid(os.getenv("CLICKHOUSE_PORT_TCP", '9000'))
398 399
            print(colored("\nFound hung queries in processlist:", args, "red", attrs=["bold"]))
            print(processlist)
400
            if server_pid:
401 402
                print("\nStacktraces of all threads:")
                print(get_stacktraces(server_pid))
403
            exit_code = 1
404
        else:
P
proller 已提交
405
            print(colored("\nNo queries hung.", args, "green", attrs=["bold"]))
406 407 408

    sys.exit(exit_code)

409

A
alesapin 已提交
410
def find_binary(name):
411 412
    if os.path.exists(name) and os.access(name, os.X_OK):
        return True
A
alesapin 已提交
413 414 415 416 417 418
    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 已提交
419 420 421 422 423 424
    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

425 426

if __name__ == '__main__':
427 428 429
    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')
430
    parser.add_argument('-b', '--binary', default='clickhouse', help='Path to clickhouse binary or name of binary in PATH')
431 432 433 434 435 436 437 438 439 440 441 442
    parser.add_argument('-c', '--client', help='Client program')
    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')
    parser.add_argument('test', nargs='?', help='Optional test case name regex')
    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')
    parser.add_argument('--order', default='desc', help='Run order (asc, desc, random)')
    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 已提交
443
    parser.add_argument('--force-color', action='store_true', default=False)
444
    parser.add_argument('--database', help='Database for tests (random name test_XXXXXX by default)')
P
proller 已提交
445
    parser.add_argument('--parallel', default='1/1', help='One parallel test run number/total')
P
proller 已提交
446
    parser.add_argument('-j', '--jobs', default=1, nargs='?', type=int, help='Run all tests in parallel')
447 448

    parser.add_argument('--no-stateless', action='store_true', help='Disable all stateless tests')
449
    parser.add_argument('--no-stateful', action='store_true', help='Disable all stateful tests')
450 451 452 453 454 455 456 457
    parser.add_argument('--skip', nargs='+', help="Skip these tests")
    parser.add_argument('--no-long', action='store_false', dest='no_long', help='Do not run long tests')
    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')
458 459

    args = parser.parse_args()
460 461 462

    if args.queries is None and os.path.isdir('queries'):
        args.queries = 'queries'
P
proller 已提交
463
    elif args.queries is None:
P
proller 已提交
464 465 466 467
        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'
468 469
        if args.tmp is None:
            args.tmp = '/tmp/clickhouse-test'
A
akuzm 已提交
470 471 472
    if args.queries is None:
        print_err("Failed to detect path to the queries directory. Please specify it with '--queries' option.")
        exit(1)
P
proller 已提交
473 474
    if args.tmp is None:
        args.tmp = args.queries
475
    if args.client is None:
A
alesapin 已提交
476
        if find_binary(args.binary + '-client'):
P
proller 已提交
477
            args.client = args.binary + '-client'
A
alesapin 已提交
478
        elif find_binary(args.binary):
P
proller 已提交
479
            args.client = args.binary + ' client'
480
        else:
A
alesapin 已提交
481 482 483 484
            print("No 'clickhouse' binary found in PATH")
            parser.print_help()
            exit(1)

P
proller 已提交
485
        if args.configclient:
486 487 488 489 490
            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")
491 492 493 494
        if os.getenv("CLICKHOUSE_DATABASE"):
            args.client += ' --database=' + os.getenv("CLICKHOUSE_DATABASE")

    args.client_with_database = args.client
495 496 497 498 499 500 501 502
    if not args.database:
        def random_str(length=6):
            import random
            import string
            alphabet = string.ascii_lowercase + string.digits
            return ''.join(random.choice(alphabet) for _ in range(length))
        args.database = 'test_{suffix}'.format(suffix=random_str())
    args.client_with_database += ' --database=' + args.database
503

P
proller 已提交
504 505 506 507 508 509
    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 已提交
510 511 512
    if args.jobs is None:
        args.jobs=multiprocessing.cpu_count()

513
    main(args)