|
1 | 1 | module ParallelTestRunner |
2 | 2 |
|
3 | | -export runtests, addworkers, addworker, find_tests |
| 3 | +export runtests, addworkers, addworker, find_tests, parse_args, filter_tests! |
4 | 4 |
|
5 | 5 | using Malt |
6 | 6 | using Dates |
|
35 | 35 |
|
36 | 36 | const max_worker_rss = JULIA_TEST_MAXRSS_MB * 2^20 |
37 | 37 |
|
38 | | -# parse some command-line arguments |
39 | | -function extract_flag!(args, flag, default = nothing; typ = typeof(default)) |
40 | | - for f in args |
41 | | - if startswith(f, flag) |
42 | | - # Check if it's just `--flag` or if it's `--flag=foo` |
43 | | - if f != flag |
44 | | - val = split(f, '=')[2] |
45 | | - if !(typ === Nothing || typ <: AbstractString) |
46 | | - val = parse(typ, val) |
47 | | - end |
48 | | - else |
49 | | - val = default |
50 | | - end |
51 | | - |
52 | | - # Drop this value from our args |
53 | | - filter!(x -> x != f, args) |
54 | | - return (true, val) |
55 | | - end |
56 | | - end |
57 | | - return (false, default) |
58 | | -end |
59 | | - |
60 | 38 | function with_testset(f, testset) |
61 | 39 | @static if VERSION >= v"1.13.0-DEV.1044" |
62 | 40 | Test.@with_testset testset f() |
@@ -487,21 +465,121 @@ function find_tests(dir::String) |
487 | 465 | return tests |
488 | 466 | end |
489 | 467 |
|
| 468 | +struct ParsedArgs |
| 469 | + jobs::Union{Some{Int}, Nothing} |
| 470 | + verbose::Union{Some{Nothing}, Nothing} |
| 471 | + quickfail::Union{Some{Nothing}, Nothing} |
| 472 | + list::Union{Some{Nothing}, Nothing} |
| 473 | + |
| 474 | + custom::Dict{String,Any} |
| 475 | + |
| 476 | + positionals::Vector{String} |
| 477 | +end |
| 478 | + |
| 479 | +# parse some command-line arguments |
| 480 | +function extract_flag!(args, flag; typ = Nothing) |
| 481 | + for f in args |
| 482 | + if startswith(f, flag) |
| 483 | + # Check if it's just `--flag` or if it's `--flag=foo` |
| 484 | + val = if f == flag |
| 485 | + nothing |
| 486 | + else |
| 487 | + parts = split(f, '=') |
| 488 | + if typ === Nothing || typ <: AbstractString |
| 489 | + parts[2] |
| 490 | + else |
| 491 | + parse(typ, parts[2]) |
| 492 | + end |
| 493 | + end |
| 494 | + |
| 495 | + # Drop this value from our args |
| 496 | + filter!(x -> x != f, args) |
| 497 | + return Some(val) |
| 498 | + end |
| 499 | + end |
| 500 | + return nothing |
| 501 | +end |
| 502 | + |
| 503 | +function parse_args(args; custom::Array{String} = String[]) |
| 504 | + args = copy(args) |
| 505 | + |
| 506 | + help = extract_flag!(args, "--help") |
| 507 | + if help !== nothing |
| 508 | + println( |
| 509 | + """ |
| 510 | + Usage: runtests.jl [--help] [--list] [--jobs=N] [TESTS...] |
| 511 | +
|
| 512 | + --help Show this text. |
| 513 | + --list List all available tests. |
| 514 | + --verbose Print more information during testing. |
| 515 | + --quickfail Fail the entire run as soon as a single test errored. |
| 516 | + --jobs=N Launch `N` processes to perform tests. |
| 517 | +
|
| 518 | + Remaining arguments filter the tests that will be executed.""" |
| 519 | + ) |
| 520 | + exit(0) |
| 521 | + end |
| 522 | + |
| 523 | + jobs = extract_flag!(args, "--jobs"; typ = Int) |
| 524 | + verbose = extract_flag!(args, "--verbose") |
| 525 | + quickfail = extract_flag!(args, "--quickfail") |
| 526 | + list = extract_flag!(args, "--list") |
| 527 | + |
| 528 | + custom_args = Dict{String,Any}() |
| 529 | + for flag in custom |
| 530 | + custom_args[flag] = extract_flag!(args, "--$flag") |
| 531 | + end |
| 532 | + |
| 533 | + ## no options should remain |
| 534 | + optlike_args = filter(startswith("-"), args) |
| 535 | + if !isempty(optlike_args) |
| 536 | + error("Unknown test options `$(join(optlike_args, " "))` (try `--help` for usage instructions)") |
| 537 | + end |
| 538 | + |
| 539 | + return ParsedArgs(jobs, verbose, quickfail, list, custom_args, args) |
| 540 | +end |
| 541 | + |
| 542 | +""" |
| 543 | + filter_tests!(testsuite, args::ParsedArgs) -> Bool |
| 544 | +
|
| 545 | +Filter tests in `testsuite` based on command-line arguments in `args`. |
| 546 | +
|
| 547 | +Returns `true` if additional filtering may be done by the caller, `false` otherwise. |
| 548 | +""" |
| 549 | +function filter_tests!(testsuite, args::ParsedArgs) |
| 550 | + # the user did not request specific tests, so let the caller do its own filtering |
| 551 | + isempty(args.positionals) && return true |
| 552 | + |
| 553 | + # only select tests matching positional arguments |
| 554 | + tests = collect(keys(testsuite)) |
| 555 | + for test in tests |
| 556 | + if !any(arg -> startswith(test, arg), args.positionals) |
| 557 | + delete!(testsuite, test) |
| 558 | + end |
| 559 | + end |
| 560 | + |
| 561 | + # the user requested specific tests, so don't allow further filtering |
| 562 | + return false |
| 563 | +end |
| 564 | + |
490 | 565 | """ |
491 | | - runtests(mod::Module, ARGS; testsuite::Dict{String,Expr}=find_tests(pwd()), |
492 | | - RecordType = TestRecord, |
493 | | - init_code = :(), |
494 | | - test_worker = Returns(nothing), |
495 | | - stdout = Base.stdout, |
496 | | - stderr = Base.stderr) |
| 566 | + runtests(mod::Module, args::ParsedArgs; |
| 567 | + testsuite::Dict{String,Expr}=find_tests(pwd()), |
| 568 | + RecordType = TestRecord, |
| 569 | + init_code = :(), |
| 570 | + test_worker = Returns(nothing), |
| 571 | + stdout = Base.stdout, |
| 572 | + stderr = Base.stderr) |
| 573 | + runtests(mod::Module, ARGS; ...) |
497 | 574 |
|
498 | 575 | Run Julia tests in parallel across multiple worker processes. |
499 | 576 |
|
500 | 577 | ## Arguments |
501 | 578 |
|
502 | 579 | - `mod`: The module calling runtests |
503 | 580 | - `ARGS`: Command line arguments array, typically from `Base.ARGS`. When you run the tests |
504 | | - with `Pkg.test`, this can be changed with the `test_args` keyword argument. |
| 581 | + with `Pkg.test`, this can be changed with the `test_args` keyword argument. If the caller |
| 582 | + needs to accept args too, consider using `parse_args` to parse the arguments first. |
505 | 583 |
|
506 | 584 | Several keyword arguments are also supported: |
507 | 585 |
|
@@ -542,87 +620,57 @@ runtests(MyModule, ARGS) |
542 | 620 | # Run only tests matching "integration" |
543 | 621 | runtests(MyModule, ["integration"]) |
544 | 622 |
|
545 | | -# Customize the test suite |
546 | | -testsuite = find_tests(pwd()) |
547 | | -delete!(testsuite, "slow_test") # Remove a specific test |
548 | | -runtests(MyModule, ARGS; testsuite) |
549 | | -
|
550 | | -# Define a custom test suite manually |
| 623 | +# Define a custom test suite |
551 | 624 | testsuite = Dict( |
552 | 625 | "custom" => quote |
553 | 626 | @test 1 + 1 == 2 |
554 | 627 | end |
555 | 628 | ) |
556 | 629 | runtests(MyModule, ARGS; testsuite) |
557 | 630 |
|
558 | | -# Use custom test record type |
559 | | -runtests(MyModule, ARGS; RecordType = MyCustomTestRecord) |
| 631 | +# Customize the test suite |
| 632 | +testsuite = find_tests(pwd()) |
| 633 | +args = parse_args(ARGS) |
| 634 | +if filter_tests!(testsuite, args) |
| 635 | + # Remove a specific test |
| 636 | + delete!(testsuite, "slow_test") |
| 637 | +end |
| 638 | +runtests(MyModule, args; testsuite) |
560 | 639 | ``` |
561 | 640 |
|
562 | 641 | ## Memory Management |
563 | 642 |
|
564 | 643 | Workers are automatically recycled when they exceed memory limits to prevent out-of-memory |
565 | 644 | issues during long test runs. The memory limit is set based on system architecture. |
566 | 645 | """ |
567 | | -function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(pwd()), |
| 646 | +function runtests(mod::Module, args::ParsedArgs; |
| 647 | + testsuite::Dict{String,Expr} = find_tests(pwd()), |
568 | 648 | RecordType = TestRecord, init_code = :(), test_worker = Returns(nothing), |
569 | 649 | stdout = Base.stdout, stderr = Base.stderr) |
570 | 650 | # |
571 | 651 | # set-up |
572 | 652 | # |
573 | 653 |
|
574 | | - do_help, _ = extract_flag!(ARGS, "--help") |
575 | | - if do_help |
576 | | - println( |
577 | | - """ |
578 | | - Usage: runtests.jl [--help] [--list] [--jobs=N] [TESTS...] |
579 | | -
|
580 | | - --help Show this text. |
581 | | - --list List all available tests. |
582 | | - --verbose Print more information during testing. |
583 | | - --quickfail Fail the entire run as soon as a single test errored. |
584 | | - --jobs=N Launch `N` processes to perform tests. |
585 | | -
|
586 | | - Remaining arguments filter the tests that will be executed.""" |
587 | | - ) |
| 654 | + # list tests, if requested |
| 655 | + if args.list !== nothing |
| 656 | + println(stdout, "Available tests:") |
| 657 | + for test in keys(testsuite) |
| 658 | + println(stdout, " - $test") |
| 659 | + end |
588 | 660 | exit(0) |
589 | 661 | end |
590 | | - set_jobs, jobs = extract_flag!(ARGS, "--jobs"; typ = Int) |
591 | | - do_verbose, _ = extract_flag!(ARGS, "--verbose") |
592 | | - do_quickfail, _ = extract_flag!(ARGS, "--quickfail") |
593 | | - do_list, _ = extract_flag!(ARGS, "--list") |
594 | | - ## no options should remain |
595 | | - optlike_args = filter(startswith("-"), ARGS) |
596 | | - if !isempty(optlike_args) |
597 | | - error("Unknown test options `$(join(optlike_args, " "))` (try `--help` for usage instructions)") |
598 | | - end |
| 662 | + |
| 663 | + # filter tests |
| 664 | + filter_tests!(testsuite, args) |
599 | 665 |
|
600 | 666 | # determine test order |
601 | 667 | tests = collect(keys(testsuite)) |
602 | 668 | Random.shuffle!(tests) |
603 | 669 | historical_durations = load_test_history(mod) |
604 | 670 | sort!(tests, by = x -> -get(historical_durations, x, Inf)) |
605 | 671 |
|
606 | | - # list tests, if requested |
607 | | - if do_list |
608 | | - println(stdout, "Available tests:") |
609 | | - for test in sort(tests) |
610 | | - println(stdout, " - $test") |
611 | | - end |
612 | | - exit(0) |
613 | | - end |
614 | | - |
615 | | - # filter tests based on command-line arguments |
616 | | - if !isempty(ARGS) |
617 | | - filter!(tests) do test |
618 | | - any(arg -> startswith(test, arg), ARGS) |
619 | | - end |
620 | | - end |
621 | | - |
622 | 672 | # determine parallelism |
623 | | - if !set_jobs |
624 | | - jobs = default_njobs() |
625 | | - end |
| 673 | + jobs = something(args.jobs, default_njobs()) |
626 | 674 | jobs = clamp(jobs, 1, length(tests)) |
627 | 675 | println(stdout, "Running $jobs tests in parallel. If this is too many, specify the `--jobs=N` argument to the tests, or set the `JULIA_CPU_THREADS` environment variable.") |
628 | 676 | workers = addworkers(min(jobs, length(tests))) |
@@ -761,7 +809,7 @@ function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(p |
761 | 809 | test_name, wrkr = msg[2], msg[3] |
762 | 810 |
|
763 | 811 | # Optionally print verbose started message |
764 | | - if do_verbose |
| 812 | + if args.verbose !== nothing |
765 | 813 | clear_status() |
766 | 814 | print_test_started(RecordType, wrkr, test_name, io_ctx) |
767 | 815 | end |
@@ -868,7 +916,7 @@ function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(p |
868 | 916 | # One of Malt.TerminatedWorkerException, Malt.RemoteException, or ErrorException |
869 | 917 | @assert result isa Exception |
870 | 918 | put!(printer_channel, (:crashed, test, worker_id(wrkr))) |
871 | | - if do_quickfail |
| 919 | + if args.quickfail !== nothing |
872 | 920 | stop_work() |
873 | 921 | end |
874 | 922 |
|
@@ -977,7 +1025,7 @@ function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(p |
977 | 1025 | return testset |
978 | 1026 | end |
979 | 1027 | t1 = time() |
980 | | - o_ts = create_testset("Overall"; start=t0, stop=t1, verbose=do_verbose) |
| 1028 | + o_ts = create_testset("Overall"; start=t0, stop=t1, verbose=!isnothing(args.verbose)) |
981 | 1029 | function collect_results() |
982 | 1030 | with_testset(o_ts) do |
983 | 1031 | completed_tests = Set{String}() |
@@ -1054,6 +1102,7 @@ function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(p |
1054 | 1102 | end |
1055 | 1103 |
|
1056 | 1104 | return |
1057 | | -end # runtests |
| 1105 | +end |
| 1106 | +runtests(mod::Module, ARGS; kwargs...) = runtests(mod, parse_args(ARGS); kwargs...) |
1058 | 1107 |
|
1059 | | -end # module ParallelTestRunner |
| 1108 | +end |
0 commit comments