Skip to content

Commit bf1e35a

Browse files
Add init_worker_code for code that runs once per worker (#85)
1 parent 39c1c38 commit bf1e35a

4 files changed

Lines changed: 137 additions & 12 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ParallelTestRunner"
22
uuid = "d3525ed8-44d0-4b2c-a655-542cee43accc"
33
authors = ["Valentin Churavy <v.churavy@gmail.com>"]
4-
version = "2.1.0"
4+
version = "2.2.0"
55

66
[deps]
77
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"

docs/src/advanced.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,38 @@ end # hide
9595

9696
The `init_code` is evaluated in each test's sandbox module, so all definitions are available to your test files.
9797

98+
## Worker Initialization
99+
100+
For most situations, `init_code` described above should be used. However, if the common code takes so long to import that it makes a notable difference to run before every testset, you can use the `init_worker_code` keyword argument in [`runtests`](@ref) to have it run only once at worker initialization. However, you will also have to import the directly-used functionality in your testset module using `init_code` due to the way ParallelTestRunner.jl creates a temporary module for each testset.
101+
102+
The example below is trivial and `init_worker_code` would not be necessary if this were used in a package, but it shows how it should be used. A real use-case of this is for tests using the GPUArrays.jl test suite; including it takes about 3s, so that 3s running before every testset can add a significant amount of runtime to the various GPU backend testsuites as opposed to running once when the runner is initally created.
103+
104+
```@example mypackage
105+
using ParallelTestRunner
106+
using MyPackage
107+
108+
const init_worker_code = quote
109+
# Common code that's slow to import
110+
function complex_common_test_helper(x)
111+
return x * 2
112+
end
113+
end
114+
115+
const init_code = quote
116+
# ParallelTestRunner creates a temporary module to run
117+
# each testset. `init_code` runs in this temporary module,
118+
# but code from `init_worker_code` that will be directly
119+
# called in a testset must be explicitly included in the
120+
# module namespace.
121+
import ..complex_common_test_helper
122+
end
123+
124+
cd(test_dir) do # hide
125+
runtests(MyPackage, ARGS; init_worker_code, init_code)
126+
end # hide
127+
```
128+
The `init_worker_code` is evaluated once per worker, so all definitions can be imported for use by the test module.
129+
98130
## Custom Workers
99131

100132
For tests that require specific environment variables or Julia flags, you can use the `test_worker` keyword argument to [`runtests`](@ref) to assign tests to custom workers:
@@ -134,6 +166,11 @@ The `test_worker` function receives the test name and should return either:
134166
- A worker object (from [`addworker`](@ref)) for tests that need special configuration
135167
- `nothing` to use the default worker pool
136168

169+
!!! note
170+
If your test suite uses both a `test_worker` function and `init_worker_code` as described in a prior section,
171+
`test_worker` must also take in `init_worker_code` as a second argument. You are responsible for passing it to
172+
[`addworker`](@ref) if your `init_code` depends on any `init_worker_code` definitions.
173+
137174
## Custom Arguments
138175

139176
If your package needs to accept its own command-line arguments in addition to `ParallelTestRunner`'s options, use [`parse_args`](@ref) with custom flags:
@@ -200,7 +237,7 @@ function jltest {
200237

201238
1. **Keep tests isolated**: Each test file runs in its own module, so avoid relying on global state between tests.
202239

203-
1. **Use `init_code` for common setup**: Instead of duplicating setup code in each test file, use `init_code` to share common initialization.
240+
1. **Use `init_code` for common setup**: Instead of duplicating setup code in each test file, use `init_code` to share common initialization. For long-running initialization, consider using `init_worker_code` so that it is run only once per worker creation instead of before each test.
204241

205242
1. **Filter tests appropriately**: Use [`filter_tests!`](@ref) to respect user-specified test filters while allowing additional programmatic filtering.
206243

src/ParallelTestRunner.jl

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -432,33 +432,36 @@ function test_exe(color::Bool=false)
432432
end
433433

434434
"""
435-
addworkers(; env=Vector{Pair{String, String}}(), exename=nothing, exeflags=nothing, color::Bool=false)
435+
addworkers(; env=Vector{Pair{String, String}}(), init_worker_code = :(), exename=nothing, exeflags=nothing, color::Bool=false)
436436
437437
Add `X` worker processes.
438438
To add a single worker, use [`addworker`](@ref).
439439
440440
## Arguments
441441
- `env`: Vector of environment variable pairs to set for the worker process.
442+
- `init_worker_code`: Code use to initialize each worker. This is run only once per worker instead of once per test.
442443
- `exename`: Custom executable to use for the worker process.
443444
- `exeflags`: Custom flags to pass to the worker process.
444445
- `color`: Boolean flag to decide whether to start `julia` with `--color=yes` (if `true`) or `--color=no` (if `false`).
445446
"""
446447
addworkers(X; kwargs...) = [addworker(; kwargs...) for _ in 1:X]
447448

448449
"""
449-
addworker(; env=Vector{Pair{String, String}}(), exename=nothing, exeflags=nothing; color::Bool=false)
450+
addworker(; env=Vector{Pair{String, String}}(), init_worker_code = :(), exename=nothing, exeflags=nothing; color::Bool=false)
450451
451-
Add a single worker process.
452+
Add a single worker process.
452453
To add multiple workers, use [`addworkers`](@ref).
453454
454455
## Arguments
455456
- `env`: Vector of environment variable pairs to set for the worker process.
457+
- `init_worker_code`: Code use to initialize each worker. This is run only once per worker instead of once per test.
456458
- `exename`: Custom executable to use for the worker process.
457459
- `exeflags`: Custom flags to pass to the worker process.
458460
- `color`: Boolean flag to decide whether to start `julia` with `--color=yes` (if `true`) or `--color=no` (if `false`).
459461
"""
460462
function addworker(;
461463
env = Vector{Pair{String, String}}(),
464+
init_worker_code = :(),
462465
exename = nothing,
463466
exeflags = nothing,
464467
color::Bool = false,
@@ -476,7 +479,11 @@ function addworker(;
476479
push!(env, "JULIA_NUM_THREADS" => "1")
477480
# Malt already sets OPENBLAS_NUM_THREADS to 1
478481
push!(env, "OPENBLAS_NUM_THREADS" => "1")
479-
return PTRWorker(; exename, exeflags, env)
482+
wrkr = PTRWorker(; exename, exeflags, env)
483+
if init_worker_code != :()
484+
Malt.remote_eval_wait(Main, wrkr.w, init_worker_code)
485+
end
486+
return wrkr
480487
end
481488

482489
"""
@@ -656,6 +663,7 @@ end
656663
runtests(mod::Module, args::Union{ParsedArgs,Array{String}};
657664
testsuite::Dict{String,Expr}=find_tests(pwd()),
658665
init_code = :(),
666+
init_worker_code = :(),
659667
test_worker = Returns(nothing),
660668
stdout = Base.stdout,
661669
stderr = Base.stderr)
@@ -677,7 +685,8 @@ Several keyword arguments are also supported:
677685
By default, automatically discovers all `.jl` files in the test directory and its subdirectories.
678686
- `init_code`: Code use to initialize each test's sandbox module (e.g., import auxiliary
679687
packages, define constants, etc).
680-
- `test_worker`: Optional function that takes a test name and returns a specific worker.
688+
- `init_worker_code`: Code use to initialize each worker. This is run only once per worker instead of once per test.
689+
- `test_worker`: Optional function that takes a test name and `init_worker_code` if `init_worker_code` is defined and returns a specific worker.
681690
When returning `nothing`, the test will be assigned to any available default worker.
682691
- `stdout` and `stderr`: I/O streams to write to (default: `Base.stdout` and `Base.stderr`)
683692
@@ -754,7 +763,7 @@ issues during long test runs. The memory limit is set based on system architectu
754763
"""
755764
function runtests(mod::Module, args::ParsedArgs;
756765
testsuite::Dict{String,Expr} = find_tests(pwd()),
757-
init_code = :(), test_worker = Returns(nothing),
766+
init_code = :(), init_worker_code = :(), test_worker = Returns(nothing),
758767
stdout = Base.stdout, stderr = Base.stderr)
759768
#
760769
# set-up
@@ -987,13 +996,18 @@ function runtests(mod::Module, args::ParsedArgs;
987996
test, test_t0
988997
end
989998

990-
# if a worker failed, spawn a new one
991-
wrkr = test_worker(test)
992-
if wrkr === nothing
999+
# pass in init_worker_code to custom worker function if defined
1000+
wrkr = if init_worker_code == :()
1001+
test_worker(test)
1002+
else
1003+
test_worker(test, init_worker_code)
1004+
end
1005+
if wrkr === nothing
9931006
wrkr = p
9941007
end
1008+
# if a worker failed, spawn a new one
9951009
if wrkr === nothing || !Malt.isrunning(wrkr)
996-
wrkr = p = addworker(; io_ctx.color)
1010+
wrkr = p = addworker(; init_worker_code, io_ctx.color)
9971011
end
9981012

9991013
# run the test

test/runtests.jl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,34 @@ end
7171
@test contains(str, "SUCCESS")
7272
end
7373

74+
@testset "init worker code" begin
75+
init_worker_code = quote
76+
should_be_defined() = true
77+
78+
macro should_also_be_defined()
79+
return :(true)
80+
end
81+
end
82+
init_code = quote
83+
using Test
84+
import ..should_be_defined, ..@should_also_be_defined
85+
end
86+
87+
testsuite = Dict(
88+
"custom" => quote
89+
@test should_be_defined()
90+
@test @should_also_be_defined()
91+
end
92+
)
93+
94+
io = IOBuffer()
95+
runtests(ParallelTestRunner, ["--verbose"]; init_code, init_worker_code, testsuite, stdout=io, stderr=io)
96+
97+
str = String(take!(io))
98+
@test contains(str, r"custom .+ started at")
99+
@test contains(str, "SUCCESS")
100+
end
101+
74102
@testset "custom worker" begin
75103
function test_worker(name)
76104
if name == "needs env var"
@@ -106,6 +134,52 @@ end
106134
@test contains(str, "SUCCESS")
107135
end
108136

137+
@testset "custom worker with `init_worker_code`" begin
138+
init_worker_code = quote
139+
should_be_defined() = true
140+
end
141+
init_code = quote
142+
using Test
143+
import ..should_be_defined
144+
end
145+
function test_worker(name, init_worker_code)
146+
if name == "needs env var"
147+
return addworker(env = ["SPECIAL_ENV_VAR" => "42"]; init_worker_code)
148+
elseif name == "threads/2"
149+
return addworker(exeflags = ["--threads=2"]; init_worker_code)
150+
end
151+
return nothing
152+
end
153+
testsuite = Dict(
154+
"needs env var" => quote
155+
@test ENV["SPECIAL_ENV_VAR"] == "42"
156+
@test should_be_defined()
157+
end,
158+
"doesn't need env var" => quote
159+
@test !haskey(ENV, "SPECIAL_ENV_VAR")
160+
@test should_be_defined()
161+
end,
162+
"threads/1" => quote
163+
@test Base.Threads.nthreads() == 1
164+
@test should_be_defined()
165+
end,
166+
"threads/2" => quote
167+
@test Base.Threads.nthreads() == 2
168+
@test should_be_defined()
169+
end
170+
)
171+
172+
io = IOBuffer()
173+
runtests(ParallelTestRunner, ["--verbose"]; test_worker, init_code, init_worker_code, testsuite, stdout=io, stderr=io)
174+
175+
str = String(take!(io))
176+
@test contains(str, r"needs env var .+ started at")
177+
@test contains(str, r"doesn't need env var .+ started at")
178+
@test contains(str, r"threads/1 .+ started at")
179+
@test contains(str, r"threads/2 .+ started at")
180+
@test contains(str, "SUCCESS")
181+
end
182+
109183
@testset "failing test" begin
110184
testsuite = Dict(
111185
"failing test" => quote

0 commit comments

Comments
 (0)