diff --git a/base/scopedvalues.jl b/base/scopedvalues.jl index 39e3c2c076718..780caa4df177d 100644 --- a/base/scopedvalues.jl +++ b/base/scopedvalues.jl @@ -104,6 +104,7 @@ function Scope(scope, pair1::Pair{<:ScopedValue}, pair2::Pair{<:ScopedValue}, pa # our compiler optimization support works return Scope(Scope(scope, pair1...), pair2, pairs...) end +Scope(scope::Scope) = scope Scope(::Nothing) = nothing function Base.show(io::IO, scope::Scope) diff --git a/stdlib/Test/src/Test.jl b/stdlib/Test/src/Test.jl index 88fd055f7bd58..9830505953435 100644 --- a/stdlib/Test/src/Test.jl +++ b/stdlib/Test/src/Test.jl @@ -29,6 +29,7 @@ using Random: AbstractRNG, default_rng using InteractiveUtils: gen_call_with_extracted_types using Base: typesplit, remove_linenums! using Serialization: Serialization +using Base.ScopedValues: ScopedValue, @with const DISPLAY_FAILED = ( :isequal, @@ -976,7 +977,7 @@ if get_testset_depth() != 0 end ``` """ -function finish end +finish(ts::AbstractTestSet) = ts """ TestSetException @@ -1011,7 +1012,6 @@ end A simple fallback test set that throws immediately on a failure. """ struct FallbackTestSet <: AbstractTestSet end -fallback_testset = FallbackTestSet() struct FallbackTestSetException <: Exception msg::String @@ -1028,8 +1028,6 @@ function record(ts::FallbackTestSet, t::Union{Fail, Error}) println(t) throw(FallbackTestSetException("There was an error during testing")) end -# We don't need to do anything as we don't record anything -finish(ts::FallbackTestSet) = ts #----------------------------------------------------------------------- @@ -1691,21 +1689,11 @@ function testset_context(args, ex, source) else error("Malformed `let` expression is given") end - reverse!(contexts) - test_ex = ex.args[2] - - ex.args[2] = quote - $(map(contexts) do context - :($push_testset($(ContextTestSet)($(QuoteNode(context)), $context; $options...))) - end...) - try - $(test_ex) - finally - $(map(_->:($pop_testset()), contexts)...) - end + for context in contexts + test_ex = :($Test.@with_testset($ContextTestSet($(QuoteNode(context)), $context; $options...), $test_ex)) end - + ex.args[2] = test_ex return esc(ex) end @@ -1756,7 +1744,7 @@ function testset_beginend_call(args, tests, source) else $(testsettype)($desc; $options...) end - push_testset(ts) + # we reproduce the logic of guardseed, but this function # cannot be used as it changes slightly the semantic of @testset, # by wrapping the body in a function @@ -1764,11 +1752,13 @@ function testset_beginend_call(args, tests, source) local tls_seed_orig = copy(Random.get_tls_seed()) local tls_seed = isnothing(get_rng(ts)) ? set_rng!(ts, tls_seed_orig) : get_rng(ts) try - # default RNG is reset to its state from last `seed!()` to ease reproduce a failed test - copy!(Random.default_rng(), tls_seed) - copy!(Random.get_tls_seed(), Random.default_rng()) - let - $(esc(tests)) + @with_testset ts begin + # default RNG is reset to its state from last `seed!()` to ease reproduce a failed test + copy!(Random.default_rng(), tls_seed) + copy!(Random.get_tls_seed(), Random.default_rng()) + let + $(esc(tests)) + end end catch err err isa InterruptException && rethrow() @@ -1783,7 +1773,6 @@ function testset_beginend_call(args, tests, source) finally copy!(default_rng(), default_rng_orig) copy!(Random.get_tls_seed(), tls_seed_orig) - pop_testset() ret = finish(ts) end ret @@ -1842,22 +1831,17 @@ function testset_forloop(args, testloop, source) _check_testset($testsettype, $(QuoteNode(testsettype.args[1]))) # Trick to handle `break` and `continue` in the test code before # they can be handled properly by `finally` lowering. - if !first_iteration - pop_testset() - finish_errored = true - push!(arr, finish(ts)) - finish_errored = false - copy!(default_rng(), tls_seed) - end ts = if ($testsettype === $DefaultTestSet) && $(isa(source, LineNumberNode)) $(testsettype)($desc; source=$(QuoteNode(source.file)), $options..., rng=tls_seed) else $(testsettype)($desc; $options...) end - push_testset(ts) - first_iteration = false try - $(esc(tests)) + @with_testset ts begin + # default RNG is reset to its state from last `seed!()` to ease reproduce a failed test + copy!(Random.default_rng(), tls_seed) + $(esc(tests)) + end catch err err isa InterruptException && rethrow() # Something in the test block threw an error. Count that as an @@ -1866,30 +1850,21 @@ function testset_forloop(args, testloop, source) if !isa(err, FailFastError) record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source)))) end + finally + copy!(default_rng(), default_rng_orig) + copy!(Random.get_tls_seed(), tls_seed_orig) + push!(arr, finish(ts)) end end quote local arr = Vector{Any}() - local first_iteration = true - local ts local rng_option = get($(options), :rng, nothing) - local finish_errored = false local default_rng_orig = copy(default_rng()) local tls_seed_orig = copy(Random.get_tls_seed()) local tls_seed = isnothing(rng_option) ? copy(Random.get_tls_seed()) : rng_option - copy!(Random.default_rng(), tls_seed) - try - let - $(Expr(:for, Expr(:block, [esc(v) for v in loopvars]...), blk)) - end - finally - # Handle `return` in test body - if !first_iteration && !finish_errored - pop_testset() - push!(arr, finish(ts)) - end - copy!(default_rng(), default_rng_orig) - copy!(Random.get_tls_seed(), tls_seed_orig) + + let + $(Expr(:for, Expr(:block, [esc(v) for v in loopvars]...), blk)) end arr end @@ -1938,6 +1913,13 @@ end #----------------------------------------------------------------------- # Various helper methods for test sets +const CURRENT_TESTSET = ScopedValue{AbstractTestSet}(FallbackTestSet()) +const TESTSET_DEPTH = ScopedValue{Int}(0) + +macro with_testset(ts, expr) + :(@with(CURRENT_TESTSET => $(esc(ts)), TESTSET_DEPTH => get_testset_depth() + 1, $(esc(expr)))) +end + """ get_testset() @@ -1945,32 +1927,7 @@ Retrieve the active test set from the task's local storage. If no test set is active, use the fallback default test set. """ function get_testset() - testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[]) - return isempty(testsets) ? fallback_testset : testsets[end] -end - -""" - push_testset(ts::AbstractTestSet) - -Adds the test set to the `task_local_storage`. -""" -function push_testset(ts::AbstractTestSet) - testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[]) - push!(testsets, ts) - setindex!(task_local_storage(), testsets, :__BASETESTNEXT__) -end - -""" - pop_testset() - -Pops the last test set added to the `task_local_storage`. If there are no -active test sets, returns the fallback default test set. -""" -function pop_testset() - testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[]) - ret = isempty(testsets) ? fallback_testset : pop!(testsets) - setindex!(task_local_storage(), testsets, :__BASETESTNEXT__) - return ret + something(Base.ScopedValues.get(CURRENT_TESTSET)) end """ @@ -1979,8 +1936,7 @@ end Return the number of active test sets, not including the default test set """ function get_testset_depth() - testsets = get(task_local_storage(), :__BASETESTNEXT__, AbstractTestSet[]) - return length(testsets) + something(Base.ScopedValues.get(TESTSET_DEPTH)) end _args_and_call((args..., f)...; kwargs...) = (args, kwargs, f(args...; kwargs...)) diff --git a/test/runtests.jl b/test/runtests.jl index f0c5e1b94c376..4264df55a0917 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -368,55 +368,55 @@ cd(@__DIR__) do Test.TESTSET_PRINT_ENABLE[] = false o_ts = Test.DefaultTestSet("Overall") o_ts.time_end = o_ts.time_start + o_ts_duration # manually populate the timing - Test.push_testset(o_ts) - completed_tests = Set{String}() - for (testname, (resp,), duration) in results - push!(completed_tests, testname) - if isa(resp, Test.DefaultTestSet) - resp.time_end = resp.time_start + duration - Test.push_testset(resp) - Test.record(o_ts, resp) - Test.pop_testset() - elseif isa(resp, Test.TestSetException) - fake = Test.DefaultTestSet(testname) - fake.time_end = fake.time_start + duration - for i in 1:resp.pass - Test.record(fake, Test.Pass(:test, nothing, nothing, nothing, LineNumberNode(@__LINE__, @__FILE__))) - end - for i in 1:resp.broken - Test.record(fake, Test.Broken(:test, nothing)) - end - for t in resp.errors_and_fails - Test.record(fake, t) + Test.@with_testset o_ts begin + completed_tests = Set{String}() + for (testname, (resp,), duration) in results + push!(completed_tests, testname) + if isa(resp, Test.DefaultTestSet) + resp.time_end = resp.time_start + duration + Test.@with_testset resp begin + Test.record(o_ts, resp) + end + elseif isa(resp, Test.TestSetException) + fake = Test.DefaultTestSet(testname) + fake.time_end = fake.time_start + duration + for i in 1:resp.pass + Test.record(fake, Test.Pass(:test, nothing, nothing, nothing, LineNumberNode(@__LINE__, @__FILE__))) + end + for i in 1:resp.broken + Test.record(fake, Test.Broken(:test, nothing)) + end + for t in resp.errors_and_fails + Test.record(fake, t) + end + Test.@with_testset fake begin + Test.record(o_ts, fake) + end + else + if !isa(resp, Exception) + resp = ErrorException(string("Unknown result type : ", typeof(resp))) + end + # If this test raised an exception that is not a remote testset exception, + # i.e. not a RemoteException capturing a TestSetException that means + # the test runner itself had some problem, so we may have hit a segfault, + # deserialization errors or something similar. Record this testset as Errored. + fake = Test.DefaultTestSet(testname) + fake.time_end = fake.time_start + duration + Test.record(fake, Test.Error(:nontest_error, testname, nothing, Any[(resp, [])], LineNumberNode(1))) + Test.@with_testset fake begin + Test.record(o_ts, fake) + end end - Test.push_testset(fake) - Test.record(o_ts, fake) - Test.pop_testset() - else - if !isa(resp, Exception) - resp = ErrorException(string("Unknown result type : ", typeof(resp))) + end + for test in all_tests + (test in completed_tests) && continue + fake = Test.DefaultTestSet(test) + Test.record(fake, Test.Error(:test_interrupted, test, nothing, [("skipped", [])], LineNumberNode(1))) + Test.@with_testset fake begin + Test.record(o_ts, fake) end - # If this test raised an exception that is not a remote testset exception, - # i.e. not a RemoteException capturing a TestSetException that means - # the test runner itself had some problem, so we may have hit a segfault, - # deserialization errors or something similar. Record this testset as Errored. - fake = Test.DefaultTestSet(testname) - fake.time_end = fake.time_start + duration - Test.record(fake, Test.Error(:nontest_error, testname, nothing, Any[(resp, [])], LineNumberNode(1))) - Test.push_testset(fake) - Test.record(o_ts, fake) - Test.pop_testset() end end - for test in all_tests - (test in completed_tests) && continue - fake = Test.DefaultTestSet(test) - Test.record(fake, Test.Error(:test_interrupted, test, nothing, [("skipped", [])], LineNumberNode(1))) - Test.push_testset(fake) - Test.record(o_ts, fake) - Test.pop_testset() - end - if Base.get_bool_env("CI", false) @info "Writing test result data to $(@__DIR__)" write_testset_json_files(@__DIR__, o_ts) diff --git a/test/scopedvalues.jl b/test/scopedvalues.jl index e9b36d80fc2c4..865538cc66420 100644 --- a/test/scopedvalues.jl +++ b/test/scopedvalues.jl @@ -1,6 +1,7 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license using Base.ScopedValues +using Test include(joinpath(@__DIR__,"../Compiler/test/irutils.jl")) @@ -80,13 +81,13 @@ import Base.Threads: @spawn end @testset "show" begin - @test sprint(show, ScopedValue{Int}()) == "Base.ScopedValues.ScopedValue{$Int}(undefined)" - @test sprint(show, sval) == "Base.ScopedValues.ScopedValue{$Int}(1)" - @test sprint(show, Core.current_scope()) == "nothing" + @test sprint(show, ScopedValue{Int}()) == "$ScopedValue{$Int}(undefined)" + @test sprint(show, sval) == "$ScopedValue{$Int}(1)" + # @test sprint(show, Core.current_scope()) == "nothing" with(sval => 2.0) do - @test sprint(show, sval) == "Base.ScopedValues.ScopedValue{$Int}(2)" + @test sprint(show, sval) == "$ScopedValue{$Int}(2)" objid = sprint(show, Base.objectid(sval)) - @test sprint(show, Core.current_scope()) == "Base.ScopedValues.Scope(Base.ScopedValues.ScopedValue{$Int}@$objid => 2)" + # @test sprint(show, Core.current_scope()) == "Base.ScopedValues.Scope(Base.ScopedValues.ScopedValue{$Int}@$objid => 2)" end end