Skip to content

Commit 0c4a9d8

Browse files
Add ability to pass an expression to run after every testitem (#108)
* WIP: Pass expression to run after every testitem * don't nest expr * Add tests * fixup! Add tests * Add docs * fixup! fixup! Add tests * Update test/integrationtests.jl * Run test_end_expr in softscope, same as testitem * Bump version * fixup! Run test_end_expr in softscope, same as testitem * Add TestEndExpr.jl to TEST_PKGS * Don't import testsetups in test_end_expr
1 parent bebe806 commit 0c4a9d8

12 files changed

+473
-30
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ReTestItems"
22
uuid = "817f1d60-ba6b-4fd5-9520-3cf149f6a823"
3-
version = "1.16.0"
3+
version = "1.17.0"
44

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

README.md

+36-1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ By default, `Test` and the package being tested will be imported into the `@test
107107

108108
Since a `@testitem` is the block of code that will be executed, `@testitem`s cannot be nested.
109109

110+
#### Test setup
111+
110112
If some test-specific code needs to be shared by multiple `@testitem`s, this code can be placed in a `module` and marked as `@testsetup`,
111113
and the `@testitem`s can depend on it via the `setup` keyword.
112114

@@ -124,7 +126,38 @@ end
124126
end
125127
```
126128

127-
### Summary
129+
The `setup` is run once on each worker process that requires it;
130+
it is not run before every `@testitem` that depends on the setup.
131+
132+
#### Post-testitem hook
133+
134+
If there is something that should be checked after every single `@testitem`, then it's possible to pass an expression to `runtests` using the `test_end_expr` keyword.
135+
This expression will be run immediately after each `@testitem`.
136+
137+
```julia
138+
test_end_expr = quote
139+
@testset "global Foo unchanged" begin
140+
foo = get_global_foo()
141+
@test foo.changes == 0
142+
end
143+
end
144+
runtests("frozzle_tests.jl"; test_end_expr)
145+
```
146+
147+
#### Worker process start-up
148+
149+
If there is some set-up that should be done on each worker process before it is used to evaluated test-items, then it is possible to pass an expression to `runtests` via the `worker_init_expr` keyword.
150+
This expression will be run on each worker process as soon as it is started.
151+
152+
```julia
153+
nworkers = 3
154+
worker_init_expr = quote
155+
set_global_foo_memory_limit!(Sys.total_memory()/nworkers)
156+
end
157+
runtests("frobble_tests.jl"; nworkers, worker_init_expr)
158+
```
159+
160+
## Summary
128161

129162
1. Write tests inside of an `@testitem` block.
130163
- These are like an `@testset`, except that they must contain all the code they need to run;
@@ -157,6 +190,8 @@ end
157190
using ReTestItems, MyPackage
158191
runtests(MyPackage)
159192
```
193+
- Pass to `runtests` any configuration you want your tests to run with, such as `retries`, `testitem_timeout`, `nworkers`, `nworker_threads`, `worker_init_expr`, `test_end_expr`.
194+
See the `runtests` docstring for details.
160195

161196
---
162197

src/ReTestItems.jl

+41-23
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ function softscope(@nospecialize ex)
4242
return ex
4343
end
4444

45+
# Call softscope on each top-level body expr
46+
# which has the effect of the body acting like you're at the REPL or
47+
# inside a testset, except imports/using/etc all still work as expected
48+
# more info: https://docs.julialang.org/en/v1.10-dev/manual/variables-and-scoping/#on-soft-scope
49+
function softscope_all!(@nospecialize ex)
50+
for i = 1:length(ex.args)
51+
ex.args[i] = softscope(ex.args[i])
52+
end
53+
end
54+
4555
include("workers.jl")
4656
using .Workers
4757
include("macros.jl")
@@ -127,6 +137,8 @@ will be run.
127137
supported through a string (e.g. "auto,2").
128138
- `worker_init_expr::Expr`: an expression that will be evaluated on each worker process before any tests are run.
129139
Can be used to load packages or set up the environment. Must be a `:block` expression.
140+
- `test_end_expr::Expr`: an expression that will be evaluated after each testitem is run.
141+
Can be used to verify that global state is unchanged after running a test. Must be a `:block` expression.
130142
- `report::Bool=false`: If `true`, write a JUnit-format XML file summarising the test results.
131143
Can also be set using the `RETESTITEMS_REPORT` environment variable. The location at which
132144
the XML report is saved can be set using the `RETESTITEMS_REPORT_LOCATION` environment variable.
@@ -182,7 +194,8 @@ function runtests(
182194
tags::Union{Symbol,AbstractVector{Symbol},Nothing}=nothing,
183195
report::Bool=parse(Bool, get(ENV, "RETESTITEMS_REPORT", "false")),
184196
logs::Symbol=default_log_display_mode(report, nworkers),
185-
verbose_results::Bool=(logs !== :issues && isinteractive())
197+
verbose_results::Bool=(logs !== :issues && isinteractive()),
198+
test_end_expr::Expr=Expr(:block),
186199
)
187200
nworker_threads = _validated_nworker_threads(nworker_threads)
188201
paths′ = filter(paths) do p
@@ -208,10 +221,10 @@ function runtests(
208221
debuglvl = Int(debug)
209222
if debuglvl > 0
210223
LoggingExtras.withlevel(LoggingExtras.Debug; verbosity=debuglvl) do
211-
_runtests(shouldrun_combined, paths′, nworkers, nworker_threads, worker_init_expr, testitem_timeout, retries, verbose_results, debuglvl, report, logs)
224+
_runtests(shouldrun_combined, paths′, nworkers, nworker_threads, worker_init_expr, test_end_expr, testitem_timeout, retries, verbose_results, debuglvl, report, logs)
212225
end
213226
else
214-
return _runtests(shouldrun_combined, paths′, nworkers, nworker_threads, worker_init_expr, testitem_timeout, retries, verbose_results, debuglvl, report, logs)
227+
return _runtests(shouldrun_combined, paths′, nworkers, nworker_threads, worker_init_expr, test_end_expr, testitem_timeout, retries, verbose_results, debuglvl, report, logs)
215228
end
216229
end
217230

@@ -225,7 +238,7 @@ end
225238
# By tracking and reusing test environments, we can avoid this issue.
226239
const TEST_ENVS = Dict{String, String}()
227240

228-
function _runtests(shouldrun, paths, nworkers::Int, nworker_threads::String, worker_init_expr::Expr, testitem_timeout::Real, retries::Int, verbose_results::Bool, debug::Int, report::Bool, logs::Symbol)
241+
function _runtests(shouldrun, paths, nworkers::Int, nworker_threads::String, worker_init_expr::Expr, test_end_expr::Expr, testitem_timeout::Real, retries::Int, verbose_results::Bool, debug::Int, report::Bool, logs::Symbol)
229242
# Don't recursively call `runtests` e.g. if we `include` a file which calls it.
230243
# So we ignore the `runtests(...)` call in `test/runtests.jl` when `runtests(...)`
231244
# was called from the command line.
@@ -245,7 +258,7 @@ function _runtests(shouldrun, paths, nworkers::Int, nworker_threads::String, wor
245258
if is_running_test_runtests_jl(proj_file)
246259
# Assume this is `Pkg.test`, so test env already active.
247260
@debugv 2 "Running in current environment `$(Base.active_project())`"
248-
return _runtests_in_current_env(shouldrun, paths, proj_file, nworkers, nworker_threads, worker_init_expr, testitem_timeout, retries, verbose_results, debug, report, logs)
261+
return _runtests_in_current_env(shouldrun, paths, proj_file, nworkers, nworker_threads, worker_init_expr, test_end_expr, testitem_timeout, retries, verbose_results, debug, report, logs)
249262
else
250263
@debugv 1 "Activating test environment for `$proj_file`"
251264
orig_proj = Base.active_project()
@@ -258,7 +271,7 @@ function _runtests(shouldrun, paths, nworkers::Int, nworker_threads::String, wor
258271
testenv = TestEnv.activate()
259272
TEST_ENVS[proj_file] = testenv
260273
end
261-
_runtests_in_current_env(shouldrun, paths, proj_file, nworkers, nworker_threads, worker_init_expr, testitem_timeout, retries, verbose_results, debug, report, logs)
274+
_runtests_in_current_env(shouldrun, paths, proj_file, nworkers, nworker_threads, worker_init_expr, test_end_expr, testitem_timeout, retries, verbose_results, debug, report, logs)
262275
finally
263276
Base.set_active_project(orig_proj)
264277
end
@@ -267,7 +280,7 @@ function _runtests(shouldrun, paths, nworkers::Int, nworker_threads::String, wor
267280
end
268281

269282
function _runtests_in_current_env(
270-
shouldrun, paths, projectfile::String, nworkers::Int, nworker_threads, worker_init_expr::Expr,
283+
shouldrun, paths, projectfile::String, nworkers::Int, nworker_threads, worker_init_expr::Expr, test_end_expr::Expr,
271284
testitem_timeout::Real, retries::Int, verbose_results::Bool, debug::Int, report::Bool, logs::Symbol,
272285
)
273286
start_time = time()
@@ -294,7 +307,7 @@ function _runtests_in_current_env(
294307
run_number = 1
295308
max_runs = 1 + max(retries, testitem.retries)
296309
while run_number max_runs
297-
res = runtestitem(testitem, ctx; verbose_results, logs)
310+
res = runtestitem(testitem, ctx; test_end_expr, verbose_results, logs)
298311
ts = res.testset
299312
print_errors_and_captured_logs(testitem, run_number; logs)
300313
report_empty_testsets(testitem, ts)
@@ -333,7 +346,7 @@ function _runtests_in_current_env(
333346
ti = starting[i]
334347
@spawn begin
335348
with_logger(original_logger) do
336-
manage_worker($w, $proj_name, $testitems, $ti, $nworker_threads, $worker_init_expr, $testitem_timeout, $retries, $verbose_results, $debug, $report, $logs)
349+
manage_worker($w, $proj_name, $testitems, $ti, $nworker_threads, $worker_init_expr, $test_end_expr, $testitem_timeout, $retries, $verbose_results, $debug, $report, $logs)
337350
end
338351
end
339352
end
@@ -441,15 +454,15 @@ function record_test_error!(testitem, msg, elapsed_seconds::Real=0.0)
441454
end
442455

443456
function manage_worker(
444-
worker::Worker, proj_name, testitems, testitem, nworker_threads, worker_init_expr,
457+
worker::Worker, proj_name, testitems, testitem, nworker_threads, worker_init_expr::Expr, test_end_expr::Expr,
445458
timeout::Real, retries::Int, verbose_results::Bool, debug::Int, report::Bool, logs::Symbol
446459
)
447460
ntestitems = length(testitems.testitems)
448461
run_number = 1
449462
while testitem !== nothing
450463
ch = Channel{TestItemResult}(1)
451464
testitem.workerid[] = worker.pid
452-
fut = remote_eval(worker, :(ReTestItems.runtestitem($testitem, GLOBAL_TEST_CONTEXT; verbose_results=$verbose_results, logs=$(QuoteNode(logs)))))
465+
fut = remote_eval(worker, :(ReTestItems.runtestitem($testitem, GLOBAL_TEST_CONTEXT; test_end_expr=$(QuoteNode(test_end_expr)), verbose_results=$verbose_results, logs=$(QuoteNode(logs)))))
453466
max_runs = 1 + max(retries, testitem.retries)
454467
try
455468
timer = Timer(timeout) do tm
@@ -823,19 +836,22 @@ end
823836
# when `runtestitem` called directly or `@testitem` called outside of `runtests`.
824837
function runtestitem(
825838
ti::TestItem, ctx::TestContext;
826-
logs::Symbol=:eager, verbose_results::Bool=true, finish_test::Bool=true,
839+
test_end_expr::Expr=Expr(:block), logs::Symbol=:eager, verbose_results::Bool=true, finish_test::Bool=true,
827840
)
828841
name = ti.name
829842
log_testitem_start(ti, ctx.ntestitems)
830843
ts = DefaultTestSet(name; verbose=verbose_results)
831844
stats = PerfStats()
832-
# start with empty block expr and build up our @testitem module body
845+
# start with empty block expr and build up our `@testitem` and `test_end_expr` module bodies
833846
body = Expr(:block)
847+
test_end_body = Expr(:block)
834848
if ti.default_imports
835849
push!(body.args, :(using Test))
850+
push!(test_end_body.args, :(using Test))
836851
if !isempty(ctx.projectname)
837852
# this obviously assumes we're in an environment where projectname is reachable
838853
push!(body.args, :(using $(Symbol(ctx.projectname))))
854+
push!(test_end_body.args, :(using $(Symbol(ctx.projectname))))
839855
end
840856
end
841857
Test.push_testset(ts)
@@ -865,27 +881,29 @@ function runtestitem(
865881
push!(body.args, :(const $setup = $ts_mod))
866882
end
867883
@debugv 1 "Setup for test item $(repr(name)) done$(_on_worker())."
868-
# add our @testitem quoted code to module body expr
884+
885+
# add our `@testitem` quoted code to module body expr
869886
append!(body.args, ti.code.args)
870887
mod_expr = :(module $(gensym(name)) end)
871-
# replace the module body with our built up expr
872-
# we're being a bit sneaky here by calling softscope on each top-level body expr
873-
# which has the effect of test item body acting like you're at the REPL or
874-
# inside a testset, except imports/using/etc all still work as expected
875-
# more info: https://docs.julialang.org/en/v1.10-dev/manual/variables-and-scoping/#on-soft-scope
876-
for i = 1:length(body.args)
877-
body.args[i] = softscope(body.args[i])
878-
end
888+
softscope_all!(body)
879889
mod_expr.args[3] = body
890+
891+
# add the `test_end_expr` to a module to be run after the test item
892+
append!(test_end_body.args, test_end_expr.args)
893+
softscope_all!(test_end_body)
894+
test_end_mod_expr = :(module $(gensym(name * " test_end")) end)
895+
test_end_mod_expr.args[3] = test_end_body
896+
880897
# eval the testitem into a temporary module, so that all results can be GC'd
881898
# once the test is done and sent over the wire. (However, note that anonymous modules
882899
# aren't always GC'd right now: https://github.com/JuliaLang/julia/issues/48711)
883-
@debugv 1 "Evaluating test item $(repr(name))$(_on_worker())."
884900
# disabled for now since there were issues when tests tried serialize/deserialize
885901
# with things defined in an anonymous module
886902
# environment = Module()
903+
@debugv 1 "Evaluating test item $(repr(name))$(_on_worker())."
887904
_, stats = @timed_with_compilation _redirect_logs(logs == :eager ? DEFAULT_STDOUT[] : logpath(ti)) do
888905
with_source_path(() -> Core.eval(Main, mod_expr), ti.file)
906+
with_source_path(() -> Core.eval(Main, test_end_mod_expr), ti.file)
889907
nothing # return nothing as the first return value of @timed_with_compilation
890908
end
891909
@debugv 1 "Done evaluating test item $(repr(name))$(_on_worker())."

test/_integration_test_tools.jl

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# (ii) do _not_ want expected fail/errors to cause ReTestItems' tests to fail/error
77
# This is not `@test_throws` etc, because we're not testing that the code fails/errors
88
# we're testing that _the tests themselves_ fail/error.
9+
using Test
910

1011
"""
1112
EncasedTestSet(desc, results) <: AbstractTestset

0 commit comments

Comments
 (0)