Skip to content

Commit 76d5b14

Browse files
Test: show context when a let testset errors (#58727)
1 parent 1d67722 commit 76d5b14

File tree

3 files changed

+83
-10
lines changed

3 files changed

+83
-10
lines changed

stdlib/Test/src/Test.jl

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,10 @@ struct Error <: Result
214214
orig_expr::String
215215
value::String
216216
backtrace::String
217+
context::Union{Nothing, String}
217218
source::LineNumberNode
218219

219-
function Error(test_type::Symbol, orig_expr, value, bt, source::LineNumberNode)
220+
function Error(test_type::Symbol, orig_expr, value, bt, source::LineNumberNode, context::Union{Nothing, String}=nothing)
220221
if test_type === :test_error
221222
bt = scrub_exc_stack(bt, nothing, extract_file(source))
222223
end
@@ -249,8 +250,14 @@ struct Error <: Result
249250
string(orig_expr),
250251
value,
251252
bt_str,
253+
context,
252254
source)
253255
end
256+
257+
# Internal constructor for creating Error with pre-processed values (used by ContextTestSet)
258+
function Error(test_type::Symbol, orig_expr::String, value::String, backtrace::String, context::Union{Nothing, String}, source::LineNumberNode)
259+
return new(test_type, orig_expr, value, backtrace, context, source)
260+
end
254261
end
255262

256263
function Base.show(io::IO, t::Error)
@@ -268,6 +275,9 @@ function Base.show(io::IO, t::Error)
268275
elseif t.test_type === :test_error
269276
println(io, " Test threw exception")
270277
println(io, " Expression: ", t.orig_expr)
278+
if t.context !== nothing
279+
println(io, " Context: ", t.context)
280+
end
271281
# Capture error message and indent to match
272282
join(io, (" " * line for line in filter!(!isempty, split(t.backtrace, "\n"))), "\n")
273283
elseif t.test_type === :test_unbroken
@@ -752,13 +762,13 @@ function do_test(result::ExecutionResult, orig_expr)
752762
Fail(:test, orig_expr, result.data, value, nothing, result.source, false)
753763
else
754764
# If the result is non-Boolean, this counts as an Error
755-
Error(:test_nonbool, orig_expr, value, nothing, result.source)
765+
Error(:test_nonbool, orig_expr, value, nothing, result.source, nothing)
756766
end
757767
else
758768
# The predicate couldn't be evaluated without throwing an
759769
# exception, so that is an Error and not a Fail
760770
@assert isa(result, Threw)
761-
testres = Error(:test_error, orig_expr, result.exception, result.backtrace::Vector{Any}, result.source)
771+
testres = Error(:test_error, orig_expr, result.exception, result.backtrace::Vector{Any}, result.source, nothing)
762772
end
763773
isa(testres, Pass) || trigger_test_failure_break(result)
764774
record(get_testset(), testres)
@@ -771,11 +781,11 @@ function do_broken_test(result::ExecutionResult, orig_expr)
771781
value = result.value
772782
if isa(value, Bool)
773783
if value
774-
testres = Error(:test_unbroken, orig_expr, value, nothing, result.source)
784+
testres = Error(:test_unbroken, orig_expr, value, nothing, result.source, nothing)
775785
end
776786
else
777787
# If the result is non-Boolean, this counts as an Error
778-
testres = Error(:test_nonbool, orig_expr, value, nothing, result.source)
788+
testres = Error(:test_nonbool, orig_expr, value, nothing, result.source, nothing)
779789
end
780790
end
781791
record(get_testset(), testres)
@@ -1109,6 +1119,13 @@ function record(c::ContextTestSet, t::Fail)
11091119
context = t.context === nothing ? context : string(t.context, "\n ", context)
11101120
record(c.parent_ts, Fail(t.test_type, t.orig_expr, t.data, t.value, context, t.source, t.message_only))
11111121
end
1122+
function record(c::ContextTestSet, t::Error)
1123+
context = string(c.context_name, " = ", c.context)
1124+
context = t.context === nothing ? context : string(t.context, "\n ", context)
1125+
# Create a new Error with the same data but updated context using internal constructor
1126+
new_error = Error(t.test_type, t.orig_expr, t.value, t.backtrace, context, t.source)
1127+
record(c.parent_ts, new_error)
1128+
end
11121129

11131130
#-----------------------------------------------------------------------
11141131

@@ -1845,7 +1862,7 @@ function testset_beginend_call(args, tests, source)
18451862
if is_failfast_error(err)
18461863
get_testset_depth() > 1 ? rethrow() : failfast_print()
18471864
else
1848-
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
1865+
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source)), nothing))
18491866
end
18501867
finally
18511868
copy!(default_rng(), default_rng_orig)
@@ -1933,7 +1950,7 @@ function testset_forloop(args, testloop, source)
19331950
if is_failfast_error(err)
19341951
get_testset_depth() > 1 ? rethrow() : failfast_print()
19351952
else
1936-
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source))))
1953+
record(ts, Error(:nontest_error, Expr(:tuple), err, Base.current_exceptions(), $(QuoteNode(source)), nothing))
19371954
end
19381955
end
19391956
end

stdlib/Test/test/runtests.jl

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ let retval_tests = @testset NoThrowTestSet begin
390390
ts = Test.DefaultTestSet("Mock for testing retval of record(::DefaultTestSet, ::T <: Result) methods")
391391
pass_mock = Test.Pass(:test, 1, 2, 3, LineNumberNode(0, "A Pass Mock"))
392392
@test Test.record(ts, pass_mock) isa Test.Pass
393-
error_mock = Test.Error(:test, 1, 2, 3, LineNumberNode(0, "An Error Mock"))
393+
error_mock = Test.Error(:test, 1, 2, 3, LineNumberNode(0, "An Error Mock"), nothing)
394394
@test Test.record(ts, error_mock; print_result=false) isa Test.Error
395395
fail_mock = Test.Fail(:test, 1, 2, 3, nothing, LineNumberNode(0, "A Fail Mock"), false)
396396
@test Test.record(ts, fail_mock; print_result=false) isa Test.Fail
@@ -1892,3 +1892,59 @@ end
18921892
@test _escape_call(:((==).(x, y))) == (; func=Expr(:., esc(:(==))), args, kwargs, quoted_func=QuoteNode(Expr(:., :(==))))
18931893
end
18941894
end
1895+
1896+
@testset "Context display in @testset let blocks" begin
1897+
# Mock parent testset that just captures results
1898+
struct MockParentTestSet <: Test.AbstractTestSet
1899+
results::Vector{Any}
1900+
MockParentTestSet() = new([])
1901+
end
1902+
Test.record(ts::MockParentTestSet, t) = (push!(ts.results, t); t)
1903+
Test.finish(ts::MockParentTestSet) = ts
1904+
1905+
@testset "context shown when a context testset fails" begin
1906+
mock_parent1 = MockParentTestSet()
1907+
ctx_ts1 = Test.ContextTestSet(mock_parent1, :x, 42)
1908+
1909+
fail_result = Test.Fail(:test, "x == 99", "42 == 99", "42", nothing, LineNumberNode(1, :test), false)
1910+
Test.record(ctx_ts1, fail_result)
1911+
1912+
@test length(mock_parent1.results) == 1
1913+
recorded_fail = mock_parent1.results[1]
1914+
@test recorded_fail isa Test.Fail
1915+
@test recorded_fail.context !== nothing
1916+
@test occursin("x = 42", recorded_fail.context)
1917+
end
1918+
1919+
@testset "context shown when a context testset errors" begin
1920+
mock_parent2 = MockParentTestSet()
1921+
ctx_ts2 = Test.ContextTestSet(mock_parent2, :x, 42)
1922+
1923+
# Use internal constructor to create Error with pre-processed values
1924+
error_result = Test.Error(:test_error, "error(\"test\")", "ErrorException(\"test\")", "test\nStacktrace:\n [1] error()", nothing, LineNumberNode(1, :test))
1925+
Test.record(ctx_ts2, error_result)
1926+
1927+
@test length(mock_parent2.results) == 1
1928+
recorded_error = mock_parent2.results[1]
1929+
@test recorded_error isa Test.Error
1930+
@test recorded_error.context !== nothing
1931+
@test occursin("x = 42", recorded_error.context)
1932+
1933+
# Context shows up in string representation
1934+
error_str = sprint(show, recorded_error)
1935+
@test occursin("Context:", error_str)
1936+
@test occursin("x = 42", error_str)
1937+
1938+
# Multiple variables context
1939+
mock_parent3 = MockParentTestSet()
1940+
ctx_ts3 = Test.ContextTestSet(mock_parent3, :(x, y), (42, "hello"))
1941+
1942+
error_result2 = Test.Error(:test_error, "error(\"test\")", "ErrorException(\"test\")", "test\nStacktrace:\n [1] error()", nothing, LineNumberNode(1, :test))
1943+
Test.record(ctx_ts3, error_result2)
1944+
1945+
recorded_error2 = mock_parent3.results[1]
1946+
@test recorded_error2 isa Test.Error
1947+
@test recorded_error2.context !== nothing
1948+
@test occursin("(x, y) = (42, \"hello\")", recorded_error2.context)
1949+
end
1950+
end

test/runtests.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ cd(@__DIR__) do
414414
# deserialization errors or something similar. Record this testset as Errored.
415415
fake = Test.DefaultTestSet(testname)
416416
fake.time_end = fake.time_start + duration
417-
Test.record(fake, Test.Error(:nontest_error, testname, nothing, Any[(resp, [])], LineNumberNode(1)))
417+
Test.record(fake, Test.Error(:nontest_error, testname, nothing, Any[(resp, [])], LineNumberNode(1), nothing))
418418
Test.push_testset(fake)
419419
Test.record(o_ts, fake)
420420
Test.pop_testset()
@@ -423,7 +423,7 @@ cd(@__DIR__) do
423423
for test in all_tests
424424
(test in completed_tests) && continue
425425
fake = Test.DefaultTestSet(test)
426-
Test.record(fake, Test.Error(:test_interrupted, test, nothing, [("skipped", [])], LineNumberNode(1)))
426+
Test.record(fake, Test.Error(:test_interrupted, test, nothing, [("skipped", [])], LineNumberNode(1), nothing))
427427
Test.push_testset(fake)
428428
Test.record(o_ts, fake)
429429
Test.pop_testset()

0 commit comments

Comments
 (0)