@@ -42,6 +42,16 @@ function softscope(@nospecialize ex)
42
42
return ex
43
43
end
44
44
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
+
45
55
include (" workers.jl" )
46
56
using . Workers
47
57
include (" macros.jl" )
@@ -127,6 +137,8 @@ will be run.
127
137
supported through a string (e.g. "auto,2").
128
138
- `worker_init_expr::Expr`: an expression that will be evaluated on each worker process before any tests are run.
129
139
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.
130
142
- `report::Bool=false`: If `true`, write a JUnit-format XML file summarising the test results.
131
143
Can also be set using the `RETESTITEMS_REPORT` environment variable. The location at which
132
144
the XML report is saved can be set using the `RETESTITEMS_REPORT_LOCATION` environment variable.
@@ -182,7 +194,8 @@ function runtests(
182
194
tags:: Union{Symbol,AbstractVector{Symbol},Nothing} = nothing ,
183
195
report:: Bool = parse (Bool, get (ENV , " RETESTITEMS_REPORT" , " false" )),
184
196
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 ),
186
199
)
187
200
nworker_threads = _validated_nworker_threads (nworker_threads)
188
201
paths′ = filter (paths) do p
@@ -208,10 +221,10 @@ function runtests(
208
221
debuglvl = Int (debug)
209
222
if debuglvl > 0
210
223
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)
212
225
end
213
226
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)
215
228
end
216
229
end
217
230
225
238
# By tracking and reusing test environments, we can avoid this issue.
226
239
const TEST_ENVS = Dict {String, String} ()
227
240
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 )
229
242
# Don't recursively call `runtests` e.g. if we `include` a file which calls it.
230
243
# So we ignore the `runtests(...)` call in `test/runtests.jl` when `runtests(...)`
231
244
# was called from the command line.
@@ -245,7 +258,7 @@ function _runtests(shouldrun, paths, nworkers::Int, nworker_threads::String, wor
245
258
if is_running_test_runtests_jl (proj_file)
246
259
# Assume this is `Pkg.test`, so test env already active.
247
260
@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)
249
262
else
250
263
@debugv 1 " Activating test environment for `$proj_file `"
251
264
orig_proj = Base. active_project ()
@@ -258,7 +271,7 @@ function _runtests(shouldrun, paths, nworkers::Int, nworker_threads::String, wor
258
271
testenv = TestEnv. activate ()
259
272
TEST_ENVS[proj_file] = testenv
260
273
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)
262
275
finally
263
276
Base. set_active_project (orig_proj)
264
277
end
@@ -267,7 +280,7 @@ function _runtests(shouldrun, paths, nworkers::Int, nworker_threads::String, wor
267
280
end
268
281
269
282
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 ,
271
284
testitem_timeout:: Real , retries:: Int , verbose_results:: Bool , debug:: Int , report:: Bool , logs:: Symbol ,
272
285
)
273
286
start_time = time ()
@@ -294,7 +307,7 @@ function _runtests_in_current_env(
294
307
run_number = 1
295
308
max_runs = 1 + max (retries, testitem. retries)
296
309
while run_number ≤ max_runs
297
- res = runtestitem (testitem, ctx; verbose_results, logs)
310
+ res = runtestitem (testitem, ctx; test_end_expr, verbose_results, logs)
298
311
ts = res. testset
299
312
print_errors_and_captured_logs (testitem, run_number; logs)
300
313
report_empty_testsets (testitem, ts)
@@ -333,7 +346,7 @@ function _runtests_in_current_env(
333
346
ti = starting[i]
334
347
@spawn begin
335
348
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)
337
350
end
338
351
end
339
352
end
@@ -441,15 +454,15 @@ function record_test_error!(testitem, msg, elapsed_seconds::Real=0.0)
441
454
end
442
455
443
456
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 ,
445
458
timeout:: Real , retries:: Int , verbose_results:: Bool , debug:: Int , report:: Bool , logs:: Symbol
446
459
)
447
460
ntestitems = length (testitems. testitems)
448
461
run_number = 1
449
462
while testitem != = nothing
450
463
ch = Channel {TestItemResult} (1 )
451
464
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)))))
453
466
max_runs = 1 + max (retries, testitem. retries)
454
467
try
455
468
timer = Timer (timeout) do tm
@@ -823,19 +836,22 @@ end
823
836
# when `runtestitem` called directly or `@testitem` called outside of `runtests`.
824
837
function runtestitem (
825
838
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 ,
827
840
)
828
841
name = ti. name
829
842
log_testitem_start (ti, ctx. ntestitems)
830
843
ts = DefaultTestSet (name; verbose= verbose_results)
831
844
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
833
846
body = Expr (:block )
847
+ test_end_body = Expr (:block )
834
848
if ti. default_imports
835
849
push! (body. args, :(using Test))
850
+ push! (test_end_body. args, :(using Test))
836
851
if ! isempty (ctx. projectname)
837
852
# this obviously assumes we're in an environment where projectname is reachable
838
853
push! (body. args, :(using $ (Symbol (ctx. projectname))))
854
+ push! (test_end_body. args, :(using $ (Symbol (ctx. projectname))))
839
855
end
840
856
end
841
857
Test. push_testset (ts)
@@ -865,27 +881,29 @@ function runtestitem(
865
881
push! (body. args, :(const $ setup = $ ts_mod))
866
882
end
867
883
@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
869
886
append! (body. args, ti. code. args)
870
887
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)
879
889
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
+
880
897
# eval the testitem into a temporary module, so that all results can be GC'd
881
898
# once the test is done and sent over the wire. (However, note that anonymous modules
882
899
# aren't always GC'd right now: https://github.com/JuliaLang/julia/issues/48711)
883
- @debugv 1 " Evaluating test item $(repr (name))$(_on_worker ()) ."
884
900
# disabled for now since there were issues when tests tried serialize/deserialize
885
901
# with things defined in an anonymous module
886
902
# environment = Module()
903
+ @debugv 1 " Evaluating test item $(repr (name))$(_on_worker ()) ."
887
904
_, stats = @timed_with_compilation _redirect_logs (logs == :eager ? DEFAULT_STDOUT[] : logpath (ti)) do
888
905
with_source_path (() -> Core. eval (Main, mod_expr), ti. file)
906
+ with_source_path (() -> Core. eval (Main, test_end_mod_expr), ti. file)
889
907
nothing # return nothing as the first return value of @timed_with_compilation
890
908
end
891
909
@debugv 1 " Done evaluating test item $(repr (name))$(_on_worker ()) ."
0 commit comments