| 
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