Skip to content

Commit f59b6c2

Browse files
committed
Replace test_filter/custom_tests by a single testsuite argument.
1 parent dc61eec commit f59b6c2

File tree

3 files changed

+158
-93
lines changed

3 files changed

+158
-93
lines changed

README.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,36 @@ using ParallelTestRunner
4040
runtests(MyModule, ARGS)
4141
```
4242

43-
### Filtering
43+
### Customizing the test suite
4444

45-
`runtests` takes a keyword argument that acts as a filter function
45+
By default, `runtests` automatically discovers all `.jl` files in your `test/` directory (excluding `runtests.jl` itself) using the `find_tests` function. You can customize which tests to run by providing a custom `testsuite` dictionary:
4646

4747
```julia
48-
function test_filter(test)
49-
if Sys.iswindows() && test == "ext/specialfunctions"
50-
return false
48+
# Manually define your test suite
49+
testsuite = Dict(
50+
"basic" => quote
51+
include("basic.jl")
52+
end,
53+
"advanced" => quote
54+
include("advanced.jl")
5155
end
52-
return true
56+
)
57+
58+
runtests(MyModule, ARGS; testsuite)
59+
```
60+
61+
You can also use `find_tests` to automatically discover tests and then filter or modify them:
62+
63+
```julia
64+
# Start with autodiscovered tests
65+
testsuite = find_tests(pwd())
66+
67+
# Remove tests that shouldn't run on Windows
68+
if Sys.iswindows()
69+
delete!(testsuite, "ext/specialfunctions")
5370
end
5471

55-
runtests(MyModule, ARGS; test_filter)
72+
runtests(MyModule, ARGS; testsuite)
5673
```
5774

5875
### Provide defaults

src/ParallelTestRunner.jl

Lines changed: 100 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module ParallelTestRunner
22

3-
export runtests, addworkers, addworker
3+
export runtests, addworkers, addworker, find_tests
44

55
using Malt
66
using Dates
@@ -445,9 +445,81 @@ function addworker(; env=Vector{Pair{String, String}}())
445445
end
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
464536
Several 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)
498570
runtests(MyModule, ARGS)
499571
500572
# Run only tests matching "integration"
501573
runtests(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
507589
runtests(MyModule, ARGS; RecordType = MyCustomTestRecord)
@@ -512,9 +594,9 @@ runtests(MyModule, ARGS; RecordType = MyCustomTestRecord)
512594
Workers are automatically recycled when they exceed memory limits to prevent out-of-memory
513595
issues 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

Comments
 (0)