Skip to content

Commit 0cd361c

Browse files
committed
use ScopedValues for TestSets
1 parent bb35dc9 commit 0cd361c

File tree

2 files changed

+111
-150
lines changed

2 files changed

+111
-150
lines changed

stdlib/Test/src/Test.jl

+66-105
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,6 @@ end
10041004
A simple fallback test set that throws immediately on a failure.
10051005
"""
10061006
struct FallbackTestSet <: AbstractTestSet end
1007-
fallback_testset = FallbackTestSet()
10081007

10091008
struct FallbackTestSetException <: Exception
10101009
msg::String
@@ -1639,22 +1638,11 @@ function testset_context(args, ex, source)
16391638
else
16401639
error("Malformed `let` expression is given")
16411640
end
1642-
reverse!(contexts)
1643-
1644-
test_ex = ex.args[2]
1645-
1646-
ex.args[2] = quote
1647-
$(map(contexts) do context
1648-
:($push_testset($(ContextTestSet)($(QuoteNode(context)), $context; $options...)))
1649-
end...)
1650-
try
1651-
$(test_ex)
1652-
finally
1653-
$(map(_->:($pop_testset()), contexts)...)
1654-
end
1641+
test_ex = esc(ex.args[2])
1642+
for context in reverse(contexts)
1643+
test_ex = :(@with_testset(ContextTestSet($(QuoteNode(context)), $(esc(context)); $(esc(options))...), $test_ex))
16551644
end
1656-
1657-
return esc(ex)
1645+
return test_ex
16581646
end
16591647

16601648
"""
@@ -1687,35 +1675,34 @@ function testset_beginend_call(args, tests, source)
16871675
else
16881676
$(testsettype)($desc; $options...)
16891677
end
1690-
push_testset(ts)
1691-
# we reproduce the logic of guardseed, but this function
1692-
# cannot be used as it changes slightly the semantic of @testset,
1693-
# by wrapping the body in a function
1694-
local default_rng_orig = copy(default_rng())
1695-
local tls_seed_orig = copy(Random.get_tls_seed())
1696-
try
1697-
# default RNG is reset to its state from last `seed!()` to ease reproduce a failed test
1698-
copy!(Random.default_rng(), tls_seed_orig)
1699-
let
1700-
$(esc(tests))
1701-
end
1702-
catch err
1703-
err isa InterruptException && rethrow()
1704-
# something in the test block threw an error. Count that as an
1705-
# error in this test set
1706-
trigger_test_failure_break(err)
1707-
if err isa FailFastError
1708-
get_testset_depth() > 1 ? rethrow() : failfast_print()
1709-
else
1710-
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
1678+
@with_testset ts begin
1679+
# we reproduce the logic of guardseed, but this function
1680+
# cannot be used as it changes slightly the semantic of @testset,
1681+
# by wrapping the body in a function
1682+
local default_rng_orig = copy(default_rng())
1683+
local tls_seed_orig = copy(Random.get_tls_seed())
1684+
try
1685+
# default RNG is reset to its state from last `seed!()` to ease reproduce a failed test
1686+
copy!(Random.default_rng(), tls_seed_orig)
1687+
let
1688+
$(esc(tests))
1689+
end
1690+
catch err
1691+
err isa InterruptException && rethrow()
1692+
# something in the test block threw an error. Count that as an
1693+
# error in this test set
1694+
trigger_test_failure_break(err)
1695+
if err isa FailFastError
1696+
get_testset_depth() > 1 ? rethrow() : failfast_print()
1697+
else
1698+
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
1699+
end
1700+
finally
1701+
copy!(default_rng(), default_rng_orig)
1702+
copy!(Random.get_tls_seed(), tls_seed_orig)
17111703
end
1712-
finally
1713-
copy!(default_rng(), default_rng_orig)
1714-
copy!(Random.get_tls_seed(), tls_seed_orig)
1715-
pop_testset()
1716-
ret = finish(ts)
17171704
end
1718-
ret
1705+
ts
17191706
end
17201707
# preserve outer location if possible
17211708
if tests isa Expr && tests.head === :block && !isempty(tests.args) && tests.args[1] isa LineNumberNode
@@ -1771,52 +1758,38 @@ function testset_forloop(args, testloop, source)
17711758
_check_testset($testsettype, $(QuoteNode(testsettype.args[1])))
17721759
# Trick to handle `break` and `continue` in the test code before
17731760
# they can be handled properly by `finally` lowering.
1774-
if !first_iteration
1775-
pop_testset()
1776-
finish_errored = true
1777-
push!(arr, finish(ts))
1778-
finish_errored = false
1779-
copy!(default_rng(), tls_seed_orig)
1780-
end
17811761
ts = if ($testsettype === $DefaultTestSet) && $(isa(source, LineNumberNode))
17821762
$(testsettype)($desc; source=$(QuoteNode(source.file)), $options...)
17831763
else
17841764
$(testsettype)($desc; $options...)
17851765
end
1786-
push_testset(ts)
1787-
first_iteration = false
1788-
try
1789-
$(esc(tests))
1790-
catch err
1791-
err isa InterruptException && rethrow()
1792-
# Something in the test block threw an error. Count that as an
1793-
# error in this test set
1794-
trigger_test_failure_break(err)
1795-
if !isa(err, FailFastError)
1796-
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
1766+
@with_testset ts begin
1767+
try
1768+
# default RNG is reset to its state from last `seed!()` to ease reproduce a failed test
1769+
copy!(Random.default_rng(), tls_seed_orig)
1770+
$(esc(tests))
1771+
catch err
1772+
err isa InterruptException && rethrow()
1773+
# Something in the test block threw an error. Count that as an
1774+
# error in this test set
1775+
trigger_test_failure_break(err)
1776+
if !isa(err, FailFastError)
1777+
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
1778+
end
1779+
finally
1780+
copy!(default_rng(), default_rng_orig)
1781+
copy!(Random.get_tls_seed(), tls_seed_orig)
1782+
push!(arr, ts)
17971783
end
17981784
end
17991785
end
18001786
quote
18011787
local arr = Vector{Any}()
1802-
local first_iteration = true
1803-
local ts
1804-
local finish_errored = false
18051788
local default_rng_orig = copy(default_rng())
18061789
local tls_seed_orig = copy(Random.get_tls_seed())
1807-
copy!(Random.default_rng(), tls_seed_orig)
1808-
try
1809-
let
1810-
$(Expr(:for, Expr(:block, [esc(v) for v in loopvars]...), blk))
1811-
end
1812-
finally
1813-
# Handle `return` in test body
1814-
if !first_iteration && !finish_errored
1815-
pop_testset()
1816-
push!(arr, finish(ts))
1817-
end
1818-
copy!(default_rng(), default_rng_orig)
1819-
copy!(Random.get_tls_seed(), tls_seed_orig)
1790+
local ts
1791+
let
1792+
$(Expr(:for, Expr(:block, [esc(v) for v in loopvars]...), blk))
18201793
end
18211794
arr
18221795
end
@@ -1855,39 +1828,27 @@ end
18551828
#-----------------------------------------------------------------------
18561829
# Various helper methods for test sets
18571830

1831+
const CURRENT_TESTSET = Base.ScopedValue{AbstractTestSet}(FallbackTestSet())
1832+
const TESTSET_DEPTH = Base.ScopedValue{Int}(0)
1833+
1834+
macro with_testset(ts, expr)
1835+
quote
1836+
ts = $(esc(ts))
1837+
Expr(:tryfinally,
1838+
Base.@with(CURRENT_TESTSET => ts, TESTSET_DEPTH => get_testset_depth() + 1, $(esc(expr))),
1839+
finish(ts)
1840+
)
1841+
end
1842+
end
1843+
18581844
"""
18591845
get_testset()
18601846
18611847
Retrieve the active test set from the task's local storage. If no
18621848
test set is active, use the fallback default test set.
18631849
"""
18641850
function get_testset()
1865-
testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[])
1866-
return isempty(testsets) ? fallback_testset : testsets[end]
1867-
end
1868-
1869-
"""
1870-
push_testset(ts::AbstractTestSet)
1871-
1872-
Adds the test set to the `task_local_storage`.
1873-
"""
1874-
function push_testset(ts::AbstractTestSet)
1875-
testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[])
1876-
push!(testsets, ts)
1877-
setindex!(task_local_storage(), testsets, :__BASETESTNEXT__)
1878-
end
1879-
1880-
"""
1881-
pop_testset()
1882-
1883-
Pops the last test set added to the `task_local_storage`. If there are no
1884-
active test sets, returns the fallback default test set.
1885-
"""
1886-
function pop_testset()
1887-
testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[])
1888-
ret = isempty(testsets) ? fallback_testset : pop!(testsets)
1889-
setindex!(task_local_storage(), testsets, :__BASETESTNEXT__)
1890-
return ret
1851+
something(Base.ScopedValues.get(CURRENT_TESTSET))
18911852
end
18921853

18931854
"""
@@ -1896,10 +1857,10 @@ end
18961857
Return the number of active test sets, not including the default test set
18971858
"""
18981859
function get_testset_depth()
1899-
testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[])
1900-
return length(testsets)
1860+
something(Base.ScopedValues.get(TESTSET_DEPTH))
19011861
end
19021862

1863+
19031864
_args_and_call(args...; kwargs...) = (args[1:end-1], kwargs, args[end](args[1:end-1]...; kwargs...))
19041865
_materialize_broadcasted(f, args...) = Broadcast.materialize(Broadcast.broadcasted(f, args...))
19051866

test/runtests.jl

+45-45
Original file line numberDiff line numberDiff line change
@@ -379,55 +379,55 @@ cd(@__DIR__) do
379379
Test.TESTSET_PRINT_ENABLE[] = false
380380
o_ts = Test.DefaultTestSet("Overall")
381381
o_ts.time_end = o_ts.time_start + o_ts_duration # manually populate the timing
382-
Test.push_testset(o_ts)
383-
completed_tests = Set{String}()
384-
for (testname, (resp,), duration) in results
385-
push!(completed_tests, testname)
386-
if isa(resp, Test.DefaultTestSet)
387-
resp.time_end = resp.time_start + duration
388-
Test.push_testset(resp)
389-
Test.record(o_ts, resp)
390-
Test.pop_testset()
391-
elseif isa(resp, Test.TestSetException)
392-
fake = Test.DefaultTestSet(testname)
393-
fake.time_end = fake.time_start + duration
394-
for i in 1:resp.pass
395-
Test.record(fake, Test.Pass(:test, nothing, nothing, nothing, LineNumberNode(@__LINE__, @__FILE__)))
396-
end
397-
for i in 1:resp.broken
398-
Test.record(fake, Test.Broken(:test, nothing))
399-
end
400-
for t in resp.errors_and_fails
401-
Test.record(fake, t)
382+
Test.@with_testset o_ts begin
383+
completed_tests = Set{String}()
384+
for (testname, (resp,), duration) in results
385+
push!(completed_tests, testname)
386+
if isa(resp, Test.DefaultTestSet)
387+
resp.time_end = resp.time_start + duration
388+
Test.@with_testset resp begin
389+
Test.record(o_ts, resp)
390+
end
391+
elseif isa(resp, Test.TestSetException)
392+
fake = Test.DefaultTestSet(testname)
393+
fake.time_end = fake.time_start + duration
394+
for i in 1:resp.pass
395+
Test.record(fake, Test.Pass(:test, nothing, nothing, nothing, LineNumberNode(@__LINE__, @__FILE__)))
396+
end
397+
for i in 1:resp.broken
398+
Test.record(fake, Test.Broken(:test, nothing))
399+
end
400+
for t in resp.errors_and_fails
401+
Test.record(fake, t)
402+
end
403+
Test.@with_testset fake begin
404+
Test.record(o_ts, fake)
405+
end
406+
else
407+
if !isa(resp, Exception)
408+
resp = ErrorException(string("Unknown result type : ", typeof(resp)))
409+
end
410+
# If this test raised an exception that is not a remote testset exception,
411+
# i.e. not a RemoteException capturing a TestSetException that means
412+
# the test runner itself had some problem, so we may have hit a segfault,
413+
# deserialization errors or something similar. Record this testset as Errored.
414+
fake = Test.DefaultTestSet(testname)
415+
fake.time_end = fake.time_start + duration
416+
Test.record(fake, Test.Error(:nontest_error, testname, nothing, Any[(resp, [])], LineNumberNode(1)))
417+
Test.@with_testset fake begin
418+
Test.record(o_ts, fake)
419+
end
402420
end
403-
Test.push_testset(fake)
404-
Test.record(o_ts, fake)
405-
Test.pop_testset()
406-
else
407-
if !isa(resp, Exception)
408-
resp = ErrorException(string("Unknown result type : ", typeof(resp)))
421+
end
422+
for test in all_tests
423+
(test in completed_tests) && continue
424+
fake = Test.DefaultTestSet(test)
425+
Test.record(fake, Test.Error(:test_interrupted, test, nothing, [("skipped", [])], LineNumberNode(1)))
426+
Test.@with_testset fake begin
427+
Test.record(o_ts, fake)
409428
end
410-
# If this test raised an exception that is not a remote testset exception,
411-
# i.e. not a RemoteException capturing a TestSetException that means
412-
# the test runner itself had some problem, so we may have hit a segfault,
413-
# deserialization errors or something similar. Record this testset as Errored.
414-
fake = Test.DefaultTestSet(testname)
415-
fake.time_end = fake.time_start + duration
416-
Test.record(fake, Test.Error(:nontest_error, testname, nothing, Any[(resp, [])], LineNumberNode(1)))
417-
Test.push_testset(fake)
418-
Test.record(o_ts, fake)
419-
Test.pop_testset()
420429
end
421430
end
422-
for test in all_tests
423-
(test in completed_tests) && continue
424-
fake = Test.DefaultTestSet(test)
425-
Test.record(fake, Test.Error(:test_interrupted, test, nothing, [("skipped", [])], LineNumberNode(1)))
426-
Test.push_testset(fake)
427-
Test.record(o_ts, fake)
428-
Test.pop_testset()
429-
end
430-
431431
if Base.get_bool_env("CI", false)
432432
@info "Writing test result data to $(@__DIR__)"
433433
write_testset_json_files(@__DIR__, o_ts)

0 commit comments

Comments
 (0)