|
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,139 @@ 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 | +""" |
| 504 | + parse_args(args; [custom::Array{String}]) -> ParsedArgs |
| 505 | +
|
| 506 | +Parse command-line arguments for `runtests`. Typically invoked by passing `Base.ARGS`. |
| 507 | +
|
| 508 | +Fields of this structure represent command-line options, containing `nothing` when the |
| 509 | +option was not specified, or `Some(optional_value=nothing)` when it was. |
| 510 | +
|
| 511 | +Custom arguments can be specified via the `custom` keyword argument, which should be |
| 512 | +an array of strings representing custom flag names (without the `--` prefix). Presence |
| 513 | +of these flags will be recorded in the `custom` field of the returned `ParsedArgs` object. |
| 514 | +""" |
| 515 | +function parse_args(args; custom::Array{String} = String[]) |
| 516 | + args = copy(args) |
| 517 | + |
| 518 | + help = extract_flag!(args, "--help") |
| 519 | + if help !== nothing |
| 520 | + usage = |
| 521 | + """ |
| 522 | + Usage: runtests.jl [--help] [--list] [--jobs=N] [TESTS...] |
| 523 | +
|
| 524 | + --help Show this text. |
| 525 | + --list List all available tests. |
| 526 | + --verbose Print more information during testing. |
| 527 | + --quickfail Fail the entire run as soon as a single test errored. |
| 528 | + --jobs=N Launch `N` processes to perform tests.""" |
| 529 | + |
| 530 | + if !isempty(custom) |
| 531 | + usage *= "\n\nCustom arguments:" |
| 532 | + for flag in custom |
| 533 | + usage *= "\n --$flag" |
| 534 | + end |
| 535 | + end |
| 536 | + usage *= "\n\nRemaining arguments filter the tests that will be executed." |
| 537 | + println(usage) |
| 538 | + exit(0) |
| 539 | + end |
| 540 | + |
| 541 | + jobs = extract_flag!(args, "--jobs"; typ = Int) |
| 542 | + verbose = extract_flag!(args, "--verbose") |
| 543 | + quickfail = extract_flag!(args, "--quickfail") |
| 544 | + list = extract_flag!(args, "--list") |
| 545 | + |
| 546 | + custom_args = Dict{String,Any}() |
| 547 | + for flag in custom |
| 548 | + custom_args[flag] = extract_flag!(args, "--$flag") |
| 549 | + end |
| 550 | + |
| 551 | + ## no options should remain |
| 552 | + optlike_args = filter(startswith("-"), args) |
| 553 | + if !isempty(optlike_args) |
| 554 | + error("Unknown test options `$(join(optlike_args, " "))` (try `--help` for usage instructions)") |
| 555 | + end |
| 556 | + |
| 557 | + return ParsedArgs(jobs, verbose, quickfail, list, custom_args, args) |
| 558 | +end |
| 559 | + |
| 560 | +""" |
| 561 | + filter_tests!(testsuite, args::ParsedArgs) -> Bool |
| 562 | +
|
| 563 | +Filter tests in `testsuite` based on command-line arguments in `args`. |
| 564 | +
|
| 565 | +Returns `true` if additional filtering may be done by the caller, `false` otherwise. |
| 566 | +""" |
| 567 | +function filter_tests!(testsuite, args::ParsedArgs) |
| 568 | + # the user did not request specific tests, so let the caller do its own filtering |
| 569 | + isempty(args.positionals) && return true |
| 570 | + |
| 571 | + # only select tests matching positional arguments |
| 572 | + tests = collect(keys(testsuite)) |
| 573 | + for test in tests |
| 574 | + if !any(arg -> startswith(test, arg), args.positionals) |
| 575 | + delete!(testsuite, test) |
| 576 | + end |
| 577 | + end |
| 578 | + |
| 579 | + # the user requested specific tests, so don't allow further filtering |
| 580 | + return false |
| 581 | +end |
| 582 | + |
490 | 583 | """ |
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) |
| 584 | + runtests(mod::Module, args::ParsedArgs; |
| 585 | + testsuite::Dict{String,Expr}=find_tests(pwd()), |
| 586 | + RecordType = TestRecord, |
| 587 | + init_code = :(), |
| 588 | + test_worker = Returns(nothing), |
| 589 | + stdout = Base.stdout, |
| 590 | + stderr = Base.stderr) |
| 591 | + runtests(mod::Module, ARGS; ...) |
497 | 592 |
|
498 | 593 | Run Julia tests in parallel across multiple worker processes. |
499 | 594 |
|
500 | 595 | ## Arguments |
501 | 596 |
|
502 | 597 | - `mod`: The module calling runtests |
503 | 598 | - `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. |
| 599 | + with `Pkg.test`, this can be changed with the `test_args` keyword argument. If the caller |
| 600 | + needs to accept args too, consider using `parse_args` to parse the arguments first. |
505 | 601 |
|
506 | 602 | Several keyword arguments are also supported: |
507 | 603 |
|
@@ -542,87 +638,57 @@ runtests(MyModule, ARGS) |
542 | 638 | # Run only tests matching "integration" |
543 | 639 | runtests(MyModule, ["integration"]) |
544 | 640 |
|
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 |
| 641 | +# Define a custom test suite |
551 | 642 | testsuite = Dict( |
552 | 643 | "custom" => quote |
553 | 644 | @test 1 + 1 == 2 |
554 | 645 | end |
555 | 646 | ) |
556 | 647 | runtests(MyModule, ARGS; testsuite) |
557 | 648 |
|
558 | | -# Use custom test record type |
559 | | -runtests(MyModule, ARGS; RecordType = MyCustomTestRecord) |
| 649 | +# Customize the test suite |
| 650 | +testsuite = find_tests(pwd()) |
| 651 | +args = parse_args(ARGS) |
| 652 | +if filter_tests!(testsuite, args) |
| 653 | + # Remove a specific test |
| 654 | + delete!(testsuite, "slow_test") |
| 655 | +end |
| 656 | +runtests(MyModule, args; testsuite) |
560 | 657 | ``` |
561 | 658 |
|
562 | 659 | ## Memory Management |
563 | 660 |
|
564 | 661 | Workers are automatically recycled when they exceed memory limits to prevent out-of-memory |
565 | 662 | issues during long test runs. The memory limit is set based on system architecture. |
566 | 663 | """ |
567 | | -function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(pwd()), |
| 664 | +function runtests(mod::Module, args::ParsedArgs; |
| 665 | + testsuite::Dict{String,Expr} = find_tests(pwd()), |
568 | 666 | RecordType = TestRecord, init_code = :(), test_worker = Returns(nothing), |
569 | 667 | stdout = Base.stdout, stderr = Base.stderr) |
570 | 668 | # |
571 | 669 | # set-up |
572 | 670 | # |
573 | 671 |
|
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 | | - ) |
| 672 | + # list tests, if requested |
| 673 | + if args.list !== nothing |
| 674 | + println(stdout, "Available tests:") |
| 675 | + for test in keys(testsuite) |
| 676 | + println(stdout, " - $test") |
| 677 | + end |
588 | 678 | exit(0) |
589 | 679 | 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 |
| 680 | + |
| 681 | + # filter tests |
| 682 | + filter_tests!(testsuite, args) |
599 | 683 |
|
600 | 684 | # determine test order |
601 | 685 | tests = collect(keys(testsuite)) |
602 | 686 | Random.shuffle!(tests) |
603 | 687 | historical_durations = load_test_history(mod) |
604 | 688 | sort!(tests, by = x -> -get(historical_durations, x, Inf)) |
605 | 689 |
|
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 | 690 | # determine parallelism |
623 | | - if !set_jobs |
624 | | - jobs = default_njobs() |
625 | | - end |
| 691 | + jobs = something(args.jobs, default_njobs()) |
626 | 692 | jobs = clamp(jobs, 1, length(tests)) |
627 | 693 | 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 | 694 | workers = addworkers(min(jobs, length(tests))) |
@@ -761,7 +827,7 @@ function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(p |
761 | 827 | test_name, wrkr = msg[2], msg[3] |
762 | 828 |
|
763 | 829 | # Optionally print verbose started message |
764 | | - if do_verbose |
| 830 | + if args.verbose !== nothing |
765 | 831 | clear_status() |
766 | 832 | print_test_started(RecordType, wrkr, test_name, io_ctx) |
767 | 833 | end |
@@ -868,7 +934,7 @@ function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(p |
868 | 934 | # One of Malt.TerminatedWorkerException, Malt.RemoteException, or ErrorException |
869 | 935 | @assert result isa Exception |
870 | 936 | put!(printer_channel, (:crashed, test, worker_id(wrkr))) |
871 | | - if do_quickfail |
| 937 | + if args.quickfail !== nothing |
872 | 938 | stop_work() |
873 | 939 | end |
874 | 940 |
|
@@ -977,7 +1043,7 @@ function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(p |
977 | 1043 | return testset |
978 | 1044 | end |
979 | 1045 | t1 = time() |
980 | | - o_ts = create_testset("Overall"; start=t0, stop=t1, verbose=do_verbose) |
| 1046 | + o_ts = create_testset("Overall"; start=t0, stop=t1, verbose=!isnothing(args.verbose)) |
981 | 1047 | function collect_results() |
982 | 1048 | with_testset(o_ts) do |
983 | 1049 | completed_tests = Set{String}() |
@@ -1054,6 +1120,7 @@ function runtests(mod::Module, ARGS; testsuite::Dict{String,Expr} = find_tests(p |
1054 | 1120 | end |
1055 | 1121 |
|
1056 | 1122 | return |
1057 | | -end # runtests |
| 1123 | +end |
| 1124 | +runtests(mod::Module, ARGS; kwargs...) = runtests(mod, parse_args(ARGS); kwargs...) |
1058 | 1125 |
|
1059 | | -end # module ParallelTestRunner |
| 1126 | +end |
0 commit comments