Skip to content

Commit bb36851

Browse files
simonbyrneKeno
andauthored
use ScopedValues for TestSets (#53462)
Another attempt at #51012 --------- Co-authored-by: Keno Fischer <[email protected]>
1 parent 1031884 commit bb36851

File tree

6 files changed

+108
-154
lines changed

6 files changed

+108
-154
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Standard library changes
8686
* Test failures when using the `@test` macro now show evaluated arguments for all function calls ([#57825], [#57839]).
8787
* Transparent test sets (`@testset let`) now show context when tests error ([#58727]).
8888
* `@test_throws` now supports a three-argument form `@test_throws ExceptionType pattern expr` to test both exception type and message pattern in one call ([#59117]).
89+
* The testset stack was changed to use `ScopedValue` rather than task local storage ([#53462]).
8990

9091
#### InteractiveUtils
9192

stdlib/Test/src/Test.jl

Lines changed: 42 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@ using Random: AbstractRNG, default_rng
2929
using InteractiveUtils: gen_call_with_extracted_types
3030
using Base: typesplit, remove_linenums!
3131
using Serialization: Serialization
32-
33-
const FAIL_FAST = Ref{Bool}(false)
32+
using Base.ScopedValues: ScopedValue, @with
3433

3534
const record_passes = OncePerProcess{Bool}() do
3635
return Base.get_bool_env("JULIA_TEST_RECORD_PASSES", false)
3736
end
3837

38+
const global_fail_fast = OncePerProcess{Bool}() do
39+
return Base.get_bool_env("JULIA_TEST_FAILFAST", false)
40+
end
41+
3942
#-----------------------------------------------------------------------
4043

4144
# Backtrace utility functions
@@ -1109,7 +1112,7 @@ if get_testset_depth() != 0
11091112
end
11101113
```
11111114
"""
1112-
function finish end
1115+
finish(ts::AbstractTestSet) = ts
11131116

11141117
"""
11151118
TestSetException
@@ -1144,7 +1147,6 @@ end
11441147
A simple fallback test set that throws immediately on a failure.
11451148
"""
11461149
struct FallbackTestSet <: AbstractTestSet end
1147-
const fallback_testset = FallbackTestSet()
11481150

11491151
struct FallbackTestSetException <: Exception
11501152
msg::String
@@ -1161,8 +1163,6 @@ function record(ts::FallbackTestSet, t::Union{Fail, Error})
11611163
println(t)
11621164
throw(FallbackTestSetException("There was an error during testing"))
11631165
end
1164-
# We don't need to do anything as we don't record anything
1165-
finish(ts::FallbackTestSet) = ts
11661166

11671167
#-----------------------------------------------------------------------
11681168

@@ -1237,7 +1237,7 @@ function DefaultTestSet(desc::AbstractString; verbose::Bool = false, showtiming:
12371237
if parent_ts isa DefaultTestSet
12381238
failfast = parent_ts.failfast
12391239
else
1240-
failfast = false
1240+
failfast = global_fail_fast()
12411241
end
12421242
end
12431243
return DefaultTestSet(String(desc)::String,
@@ -1281,7 +1281,7 @@ function record(ts::DefaultTestSet, t::Union{Fail, Error}; print_result::Bool=TE
12811281
end
12821282
end
12831283
@lock ts.results_lock push!(ts.results, t)
1284-
(FAIL_FAST[] || ts.failfast) && throw(FailFastError())
1284+
ts.failfast && throw(FailFastError())
12851285
return t
12861286
end
12871287

@@ -1408,9 +1408,6 @@ function print_test_results(io::IO, ts::AbstractTestSet, depth_pad=0)
14081408
end
14091409
end
14101410

1411-
1412-
const TESTSET_PRINT_ENABLE = Ref(true)
1413-
14141411
# Called at the end of a @testset, behaviour depends on whether
14151412
# this is a child of another testset, or the "root" testset
14161413
function finish(ts::DefaultTestSet; print_results::Bool=TESTSET_PRINT_ENABLE[])
@@ -1836,8 +1833,6 @@ macro testset(args...)
18361833
error("Expected function call, begin/end block or for loop as argument to @testset")
18371834
end
18381835

1839-
FAIL_FAST[] = Base.get_bool_env("JULIA_TEST_FAILFAST", false)
1840-
18411836
if tests.head === :for
18421837
return testset_forloop(args, tests, __source__)
18431838
elseif tests.head === :let
@@ -1881,21 +1876,11 @@ function testset_context(args, ex, source)
18811876
else
18821877
error("Malformed `let` expression is given")
18831878
end
1884-
reverse!(contexts)
1885-
18861879
test_ex = ex.args[2]
1887-
1888-
ex.args[2] = quote
1889-
$(map(contexts) do context
1890-
:($push_testset($(ContextTestSet)($(QuoteNode(context)), $context; $options...)))
1891-
end...)
1892-
try
1893-
$(test_ex)
1894-
finally
1895-
$(map(_->:($pop_testset()), contexts)...)
1896-
end
1880+
for context in contexts
1881+
test_ex = :($Test.@with_testset($ContextTestSet($(QuoteNode(context)), $context; $options...), $test_ex))
18971882
end
1898-
1883+
ex.args[2] = test_ex
18991884
return esc(ex)
19001885
end
19011886

@@ -1946,7 +1931,7 @@ function testset_beginend_call(args, tests, source)
19461931
else
19471932
$(testsettype)($desc; $options...)
19481933
end
1949-
push_testset(ts)
1934+
19501935
# we reproduce the logic of guardseed, but this function
19511936
# cannot be used as it changes slightly the semantic of @testset,
19521937
# by wrapping the body in a function
@@ -1955,26 +1940,27 @@ function testset_beginend_call(args, tests, source)
19551940
local ts_rng = get_rng(ts)
19561941
local tls_seed = isnothing(ts_rng) ? set_rng!(ts, tls_seed_orig) : ts_rng
19571942
try
1958-
# default RNG is reset to its state from last `seed!()` to ease reproduce a failed test
1959-
copy!(Random.default_rng(), tls_seed)
1960-
copy!(Random.get_tls_seed(), Random.default_rng())
1961-
let
1962-
$(esc(tests))
1943+
@with_testset ts begin
1944+
# default RNG is reset to its state from last `seed!()` to ease reproduce a failed test
1945+
copy!(Random.default_rng(), tls_seed)
1946+
copy!(Random.get_tls_seed(), Random.default_rng())
1947+
let
1948+
$(esc(tests))
1949+
end
19631950
end
19641951
catch err
19651952
err isa InterruptException && rethrow()
19661953
# something in the test block threw an error. Count that as an
19671954
# error in this test set
19681955
trigger_test_failure_break(err)
19691956
if is_failfast_error(err)
1970-
get_testset_depth() > 1 ? rethrow() : failfast_print()
1957+
get_testset_depth() > 0 ? rethrow() : failfast_print()
19711958
else
19721959
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source)), nothing))
19731960
end
19741961
finally
19751962
copy!(default_rng(), default_rng_orig)
19761963
copy!(Random.get_tls_seed(), tls_seed_orig)
1977-
pop_testset()
19781964
ret = finish(ts)
19791965
end
19801966
ret
@@ -2031,59 +2017,41 @@ function testset_forloop(args, testloop, source)
20312017
tests = testloop.args[2]
20322018
blk = quote
20332019
_check_testset($testsettype, $(QuoteNode(testsettype.args[1])))
2034-
# Trick to handle `break` and `continue` in the test code before
2035-
# they can be handled properly by `finally` lowering.
2036-
if !first_iteration
2037-
pop_testset()
2038-
finish_errored = true
2039-
push!(arr, finish(ts))
2040-
finish_errored = false
2041-
copy!(default_rng(), tls_seed)
2042-
end
20432020
ts = if ($testsettype === $DefaultTestSet) && $(isa(source, LineNumberNode))
20442021
$(testsettype)($desc; source=$(QuoteNode(source.file)), $options..., rng=tls_seed)
20452022
else
20462023
$(testsettype)($desc; $options...)
20472024
end
2048-
push_testset(ts)
2049-
first_iteration = false
20502025
try
2051-
$(esc(tests))
2026+
@with_testset ts begin
2027+
# default RNG is reset to its state from last `seed!()` to ease reproduce a failed test
2028+
copy!(Random.default_rng(), tls_seed)
2029+
$(esc(tests))
2030+
end
20522031
catch err
20532032
err isa InterruptException && rethrow()
20542033
# Something in the test block threw an error. Count that as an
20552034
# error in this test set
20562035
trigger_test_failure_break(err)
20572036
if is_failfast_error(err)
2058-
get_testset_depth() > 1 ? rethrow() : failfast_print()
2037+
get_testset_depth() > 0 ? rethrow() : failfast_print()
20592038
else
20602039
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source)), nothing))
20612040
end
2041+
finally
2042+
copy!(default_rng(), default_rng_orig)
2043+
copy!(Random.get_tls_seed(), tls_seed_orig)
2044+
push!(arr, finish(ts))
20622045
end
20632046
end
20642047
quote
20652048
local arr = Vector{Any}()
2066-
local first_iteration = true
2067-
local ts
20682049
local rng_option = get($(options), :rng, nothing)
2069-
local finish_errored = false
20702050
local default_rng_orig = copy(default_rng())
20712051
local tls_seed_orig = copy(Random.get_tls_seed())
20722052
local tls_seed = isnothing(rng_option) ? copy(Random.get_tls_seed()) : rng_option
2073-
copy!(Random.default_rng(), tls_seed)
2074-
try
2075-
let
2076-
$(Expr(:for, Expr(:block, [esc(v) for v in loopvars]...), blk))
2077-
end
2078-
finally
2079-
# Handle `return` in test body
2080-
if !first_iteration && !finish_errored
2081-
pop_testset()
2082-
@assert @isdefined(ts) "Assertion to tell the compiler about the definedness of this variable"
2083-
push!(arr, finish(ts))
2084-
end
2085-
copy!(default_rng(), default_rng_orig)
2086-
copy!(Random.get_tls_seed(), tls_seed_orig)
2053+
let
2054+
$(Expr(:for, Expr(:block, [esc(v) for v in loopvars]...), blk))
20872055
end
20882056
arr
20892057
end
@@ -2132,39 +2100,22 @@ end
21322100
#-----------------------------------------------------------------------
21332101
# Various helper methods for test sets
21342102

2103+
const CURRENT_TESTSET = ScopedValue{AbstractTestSet}(FallbackTestSet())
2104+
const TESTSET_DEPTH = ScopedValue{Int}(0)
2105+
const TESTSET_PRINT_ENABLE = ScopedValue{Bool}(true)
2106+
2107+
macro with_testset(ts, expr)
2108+
:(@with(CURRENT_TESTSET => $(esc(ts)), TESTSET_DEPTH => get_testset_depth() + 1, $(esc(expr))))
2109+
end
2110+
21352111
"""
21362112
get_testset()
21372113
21382114
Retrieve the active test set from the task's local storage. If no
21392115
test set is active, use the fallback default test set.
21402116
"""
21412117
function get_testset()
2142-
testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[])
2143-
return isempty(testsets) ? fallback_testset : testsets[end]
2144-
end
2145-
2146-
"""
2147-
push_testset(ts::AbstractTestSet)
2148-
2149-
Adds the test set to the `task_local_storage`.
2150-
"""
2151-
function push_testset(ts::AbstractTestSet)
2152-
testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[])
2153-
push!(testsets, ts)
2154-
setindex!(task_local_storage(), testsets, :__BASETESTNEXT__)
2155-
end
2156-
2157-
"""
2158-
pop_testset()
2159-
2160-
Pops the last test set added to the `task_local_storage`. If there are no
2161-
active test sets, returns the fallback default test set.
2162-
"""
2163-
function pop_testset()
2164-
testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[])
2165-
ret = isempty(testsets) ? fallback_testset : pop!(testsets)
2166-
setindex!(task_local_storage(), testsets, :__BASETESTNEXT__)
2167-
return ret
2118+
something(Base.ScopedValues.get(CURRENT_TESTSET))
21682119
end
21692120

21702121
"""
@@ -2173,8 +2124,7 @@ end
21732124
Return the number of active test sets, not including the default test set
21742125
"""
21752126
function get_testset_depth()
2176-
testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[])
2177-
return length(testsets)
2127+
something(Base.ScopedValues.get(TESTSET_DEPTH))
21782128
end
21792129

21802130
_args_and_call((args..., f)...; kwargs...) = (args, kwargs, f(args...; kwargs...))

stdlib/Test/test/runtests.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,7 +1541,7 @@ end
15411541
write(f,
15421542
"""
15431543
using Test
1544-
ENV["JULIA_TEST_FAILFAST"] = true
1544+
15451545
@testset "Foo" begin
15461546
@test false
15471547
@test error()
@@ -1551,7 +1551,7 @@ end
15511551
end
15521552
end
15531553
""")
1554-
cmd = `$(Base.julia_cmd()) --startup-file=no --color=no $f`
1554+
cmd = addenv(`$(Base.julia_cmd()) --startup-file=no --color=no $f`, "JULIA_TEST_FAILFAST"=>"true")
15551555
result = read(pipeline(ignorestatus(cmd), stderr=devnull), String)
15561556
@test occursin(expected, result)
15571557
end

0 commit comments

Comments
 (0)