11module ParallelTestRunner
22
3- export runtests, addworkers, addworker
3+ export runtests, addworkers, addworker, find_tests
44
55using Malt
66using Dates
@@ -445,9 +445,81 @@ function addworker(; env=Vector{Pair{String, String}}())
445445end
446446
447447"""
448- runtests(mod::Module, ARGS; RecordType = TestRecord,
449- test_filter = Returns(true),
450- custom_tests = Dict(),
448+ find_tests([f], dir::String) -> Dict{String, Expr}
449+
450+ Discover test files in a directory and return a test suite dictionary.
451+
452+ Walks through `dir` and finds all `.jl` files (excluding `runtests.jl`), returning a
453+ dictionary mapping test names to expressions that run those tests.
454+
455+ ## Arguments
456+
457+ - `f`: Optional function that takes a file path and returns an expression to execute
458+ (default: `path -> :(include(\$ path))`)
459+ - `dir`: Directory to search for test files
460+
461+ ## Returns
462+
463+ A `Dict{String, Expr}` where keys are test names (file paths relative to `dir` with
464+ `.jl` extension removed and path separators normalized to `/`) and values are expressions
465+ to execute for each test.
466+
467+ ## Examples
468+
469+ ```julia
470+ # Auto-discover tests with default include behavior
471+ testsuite = find_tests(pwd())
472+
473+ # Custom expression for each test file
474+ testsuite = find_tests(pwd()) do path
475+ quote
476+ @info "Running test: \$ path"
477+ include(\$ path)
478+ end
479+ end
480+ ```
481+ """
482+ function find_tests (f, dir:: String )
483+ tests = Dict {String, Expr} ()
484+ for (rootpath, dirs, files) in walkdir (dir)
485+ # find Julia files
486+ filter! (files) do file
487+ endswith (file, " .jl" ) && file != = " runtests.jl"
488+ end
489+ isempty (files) && continue
490+
491+ # strip extension
492+ files = map (files) do file
493+ file[1 : (end - 3 )]
494+ end
495+
496+ # prepend subdir
497+ subdir = relpath (rootpath, dir)
498+ if subdir != " ."
499+ files = map (files) do file
500+ joinpath (subdir, file)
501+ end
502+ end
503+
504+ # unify path separators
505+ files = map (files) do file
506+ replace (file, path_separator => ' /' )
507+ end
508+
509+ for file in files
510+ path = joinpath (rootpath, file * " .jl" )
511+ tests[file] = f (path)
512+ end
513+ end
514+ return tests
515+ end
516+ find_tests (dir:: String ) = find_tests (dir) do path
517+ :(include ($ path))
518+ end
519+
520+ """
521+ runtests(mod::Module, ARGS; testsuite::Dict{String,Expr}=find_tests(pwd()),
522+ RecordType = TestRecord,
451523 init_code = :(),
452524 test_worker = Returns(nothing),
453525 stdout = Base.stdout,
@@ -463,9 +535,9 @@ Run Julia tests in parallel across multiple worker processes.
463535
464536Several keyword arguments are also supported:
465537
538+ - `testsuite`: Dictionary mapping test names to expressions to execute (default: `find_tests(pwd())`).
539+ By default, automatically discovers all `.jl` files in the test directory.
466540- `RecordType`: Type of test record to use for tracking test results (default: `TestRecord`)
467- - `test_filter`: Optional function to filter which tests to run (default: run all tests)
468- - `custom_tests`: Optional dictionary of custom tests, mapping test names to expressions.
469541- `init_code`: Code use to initialize each test's sandbox module (e.g., import auxiliary
470542 packages, define constants, etc).
471543- `test_worker`: Optional function that takes a test name and returns a specific worker.
@@ -494,14 +566,24 @@ Several keyword arguments are also supported:
494566## Examples
495567
496568```julia
497- # Run all tests with default settings
569+ # Run all tests with default settings (auto-discovers .jl files)
498570runtests(MyModule, ARGS)
499571
500572# Run only tests matching "integration"
501573runtests(MyModule, ["integration"])
502574
503- # Run with custom filter function
504- runtests(MyModule, ARGS; test_filter = test -> occursin("unit", test))
575+ # Customize the test suite
576+ testsuite = find_tests(pwd())
577+ delete!(testsuite, "slow_test") # Remove a specific test
578+ runtests(MyModule, ARGS; testsuite)
579+
580+ # Define a custom test suite manually
581+ testsuite = Dict(
582+ "custom" => quote
583+ @test 1 + 1 == 2
584+ end
585+ )
586+ runtests(MyModule, ARGS; testsuite)
505587
506588# Use custom test record type
507589runtests(MyModule, ARGS; RecordType = MyCustomTestRecord)
@@ -512,9 +594,9 @@ runtests(MyModule, ARGS; RecordType = MyCustomTestRecord)
512594Workers are automatically recycled when they exceed memory limits to prevent out-of-memory
513595issues during long test runs. The memory limit is set based on system architecture.
514596"""
515- function runtests (mod:: Module , ARGS ; test_filter = Returns ( true ), RecordType = TestRecord ,
516- custom_tests :: Dict{String, Expr} = Dict {String, Expr} (), init_code = :( ),
517- test_worker = Returns ( nothing ), stdout = Base. stdout , stderr = Base. stderr )
597+ function runtests (mod:: Module , ARGS ; testsuite :: Dict{String,Expr} = find_tests ( pwd ()) ,
598+ RecordType = TestRecord, init_code = : (), test_worker = Returns ( nothing ),
599+ stdout = Base. stdout , stderr = Base. stderr )
518600 #
519601 # set-up
520602 #
@@ -545,51 +627,8 @@ function runtests(mod::Module, ARGS; test_filter = Returns(true), RecordType = T
545627 error (" Unknown test options `$(join (optlike_args, " " )) ` (try `--help` for usage instructions)" )
546628 end
547629
548- WORKDIR = pwd ()
549-
550- # choose tests
551- tests = []
552- test_runners = Dict ()
553- # # custom tests by the user
554- for (name, runner) in custom_tests
555- push! (tests, name)
556- test_runners[name] = runner
557- end
558- # # files in the test folder
559- for (rootpath, dirs, files) in walkdir (WORKDIR)
560- # find Julia files
561- filter! (files) do file
562- endswith (file, " .jl" ) && file != = " runtests.jl"
563- end
564- isempty (files) && continue
565-
566- # strip extension
567- files = map (files) do file
568- file[1 : (end - 3 )]
569- end
570-
571- # prepend subdir
572- subdir = relpath (rootpath, WORKDIR)
573- if subdir != " ."
574- files = map (files) do file
575- joinpath (subdir, file)
576- end
577- end
578-
579- # unify path separators
580- files = map (files) do file
581- replace (file, path_separator => ' /' )
582- end
583-
584- append! (tests, files)
585- for file in files
586- test_runners[file] = quote
587- include ($ (joinpath (WORKDIR, file * " .jl" )))
588- end
589- end
590- end
591- # # finalize
592- unique! (tests)
630+ # determine test order
631+ tests = collect (keys (testsuite))
593632 Random. shuffle! (tests)
594633 historical_durations = load_test_history (mod)
595634 sort! (tests, by = x -> - get (historical_durations, x, Inf ))
@@ -603,11 +642,8 @@ function runtests(mod::Module, ARGS; test_filter = Returns(true), RecordType = T
603642 exit (0 )
604643 end
605644
606- # filter tests
607- if isempty (ARGS )
608- filter! (test_filter, tests)
609- else
610- # let the user filter
645+ # filter tests based on command-line arguments
646+ if ! isempty (ARGS )
611647 filter! (tests) do test
612648 any (arg -> startswith (test, arg), ARGS )
613649 end
@@ -834,8 +870,8 @@ function runtests(mod::Module, ARGS; test_filter = Returns(true), RecordType = T
834870 put! (printer_channel, (:started , test, worker_id (wrkr)))
835871 result = try
836872 Malt. remote_eval_wait (Main, wrkr, :(import ParallelTestRunner))
837- Malt. remote_call_fetch (invokelatest, wrkr, runtest, RecordType, test_runners[test], test,
838- init_code, io_ctx. color)
873+ Malt. remote_call_fetch (invokelatest, wrkr, runtest, RecordType,
874+ testsuite[test], test, init_code, io_ctx. color)
839875 catch ex
840876 if isa (ex, InterruptException)
841877 # the worker got interrupted, signal other tasks to stop
0 commit comments