Skip to content

Commit 7d3dac4

Browse files
authored
Test: try really harder to show tests correctly (#37809)
By serializing both tests and backtraces (as strings), and trying to ensure other parts are not going to mess them up along the way. Hopefully this should improve error message reporting reliability in our Base runtests.
1 parent c402113 commit 7d3dac4

File tree

6 files changed

+114
-71
lines changed

6 files changed

+114
-71
lines changed

stdlib/Test/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name = "Test"
22
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
33

44
[deps]
5-
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
65
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
76
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
87
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
8+
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

stdlib/Test/src/Test.jl

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@ export detect_ambiguities, detect_unbound_args
2323
export GenericString, GenericSet, GenericDict, GenericArray, GenericOrder
2424
export TestSetException
2525

26-
import Distributed: myid
27-
2826
using Random
2927
using Random: AbstractRNG, default_rng
3028
using InteractiveUtils: gen_call_with_extracted_types
3129
using Base: typesplit
30+
using Serialization: Serialization
3231

3332
const DISPLAY_FAILED = (
3433
:isequal,
@@ -85,15 +84,19 @@ struct Pass <: Result
8584
orig_expr
8685
data
8786
value
87+
function Pass(test_type::Symbol, orig_expr, data, thrown)
88+
return new(test_type, orig_expr, data, thrown isa String ? "String" : thrown)
89+
end
8890
end
91+
8992
function Base.show(io::IO, t::Pass)
9093
printstyled(io, "Test Passed"; bold = true, color=:green)
9194
if !(t.orig_expr === nothing)
9295
print(io, "\n Expression: ", t.orig_expr)
9396
end
9497
if t.test_type === :test_throws
9598
# The correct type of exception was thrown
96-
print(io, "\n Thrown: ", typeof(t.value))
99+
print(io, "\n Thrown: ", t.value isa String ? t.value : typeof(t.value))
97100
elseif t.test_type === :test && t.data !== nothing
98101
# The test was an expression, so display the term-by-term
99102
# evaluated version as well
@@ -107,13 +110,21 @@ end
107110
The test condition was false, i.e. the expression evaluated to false or
108111
the correct exception was not thrown.
109112
"""
110-
mutable struct Fail <: Result
113+
struct Fail <: Result
111114
test_type::Symbol
112-
orig_expr
113-
data
114-
value
115+
orig_expr::String
116+
data::Union{Nothing, String}
117+
value::String
115118
source::LineNumberNode
119+
function Fail(test_type::Symbol, orig_expr, data, value, source::LineNumberNode)
120+
return new(test_type,
121+
string(orig_expr),
122+
data === nothing ? nothing : string(data),
123+
string(isa(data, Type) ? typeof(value) : value),
124+
source)
125+
end
116126
end
127+
117128
function Base.show(io::IO, t::Fail)
118129
printstyled(io, "Test Failed"; bold=true, color=Base.error_color())
119130
print(io, " at ")
@@ -122,7 +133,7 @@ function Base.show(io::IO, t::Fail)
122133
if t.test_type === :test_throws_wrong
123134
# An exception was thrown, but it was of the wrong type
124135
print(io, "\n Expected: ", t.data)
125-
print(io, "\n Thrown: ", isa(t.data, Type) ? typeof(t.value) : t.value)
136+
print(io, "\n Thrown: ", t.value)
126137
elseif t.test_type === :test_throws_nothing
127138
# An exception was expected, but no exception was thrown
128139
print(io, "\n Expected: ", t.data)
@@ -142,10 +153,10 @@ it evaluated to something other than a [`Bool`](@ref).
142153
In the case of `@test_broken` it is used to indicate that an
143154
unexpected `Pass` `Result` occurred.
144155
"""
145-
mutable struct Error <: Result
156+
struct Error <: Result
146157
test_type::Symbol
147-
orig_expr
148-
value
158+
orig_expr::String
159+
value::String
149160
backtrace::String
150161
source::LineNumberNode
151162

@@ -158,13 +169,14 @@ mutable struct Error <: Result
158169
else
159170
bt_str = ""
160171
end
161-
new(test_type,
162-
orig_expr,
172+
return new(test_type,
173+
string(orig_expr),
163174
sprint(show, value, context = :limit => true),
164175
bt_str,
165176
source)
166177
end
167178
end
179+
168180
function Base.show(io::IO, t::Error)
169181
if t.test_type === :test_interrupted
170182
printstyled(io, "Interrupted", color=Base.error_color())
@@ -201,10 +213,11 @@ end
201213
The test condition is the expected (failed) result of a broken test,
202214
or was explicitly skipped with `@test_skip`.
203215
"""
204-
mutable struct Broken <: Result
216+
struct Broken <: Result
205217
test_type::Symbol
206218
orig_expr
207219
end
220+
208221
function Base.show(io::IO, t::Broken)
209222
printstyled(io, "Test Broken\n"; bold=true, color=Base.warn_color())
210223
if t.test_type === :skipped && !(t.orig_expr === nothing)
@@ -214,6 +227,25 @@ function Base.show(io::IO, t::Broken)
214227
end
215228
end
216229

230+
# Types that appear in TestSetException.errors_and_fails we convert eagerly into strings
231+
# other types we convert lazily
232+
function Serialization.serialize(s::Serialization.AbstractSerializer, t::Pass)
233+
Serialization.serialize_type(s, typeof(t))
234+
Serialization.serialize(s, t.test_type)
235+
Serialization.serialize(s, t.orig_expr === nothing ? nothing : string(t.orig_expr))
236+
Serialization.serialize(s, t.data === nothing ? nothing : string(t.data))
237+
Serialization.serialize(s, string(t.value))
238+
nothing
239+
end
240+
241+
function Serialization.serialize(s::Serialization.AbstractSerializer, t::Broken)
242+
Serialization.serialize_type(s, typeof(t))
243+
Serialization.serialize(s, t.test_type)
244+
Serialization.serialize(s, t.orig_expr === nothing ? nothing : string(t.orig_expr))
245+
nothing
246+
end
247+
248+
217249
#-----------------------------------------------------------------------
218250

219251
abstract type ExecutionResult end
@@ -730,8 +762,8 @@ end
730762

731763
# Records nothing, and throws an error immediately whenever a Fail or
732764
# Error occurs. Takes no action in the event of a Pass or Broken result
733-
record(ts::FallbackTestSet, t::Union{Pass,Broken}) = t
734-
function record(ts::FallbackTestSet, t::Union{Fail,Error})
765+
record(ts::FallbackTestSet, t::Union{Pass, Broken}) = t
766+
function record(ts::FallbackTestSet, t::Union{Fail, Error})
735767
println(t)
736768
throw(FallbackTestSetException("There was an error during testing"))
737769
end
@@ -763,7 +795,7 @@ record(ts::DefaultTestSet, t::Pass) = (ts.n_passed += 1; t)
763795
# For the other result types, immediately print the error message
764796
# but do not terminate. Print a backtrace.
765797
function record(ts::DefaultTestSet, t::Union{Fail, Error})
766-
if myid() == 1
798+
if TESTSET_PRINT_ENABLE[]
767799
printstyled(ts.description, ": ", color=:white)
768800
# don't print for interrupted tests
769801
if !(t isa Error) || t.test_type !== :test_interrupted
@@ -775,7 +807,6 @@ function record(ts::DefaultTestSet, t::Union{Fail, Error})
775807
end
776808
end
777809
push!(ts.results, t)
778-
isa(t, Error) || backtrace()
779810
return t
780811
end
781812

@@ -788,9 +819,9 @@ record(ts::DefaultTestSet, t::AbstractTestSet) = push!(ts.results, t)
788819

789820
function print_test_errors(ts::DefaultTestSet)
790821
for t in ts.results
791-
if (isa(t, Error) || isa(t, Fail)) && myid() == 1
822+
if isa(t, Error) || isa(t, Fail)
792823
println("Error in testset $(ts.description):")
793-
Base.show(stdout,t)
824+
show(t)
794825
println()
795826
elseif isa(t, DefaultTestSet)
796827
print_test_errors(t)
@@ -874,7 +905,7 @@ function finish(ts::DefaultTestSet)
874905
if total != total_pass + total_broken
875906
# Get all the error/failures and bring them along for the ride
876907
efs = filter_errors(ts)
877-
throw(TestSetException(total_pass,total_fail,total_error, total_broken, efs))
908+
throw(TestSetException(total_pass, total_fail, total_error, total_broken, efs))
878909
end
879910

880911
# return the testset so it is returned from the @testset macro

stdlib/Test/src/logging.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function record(::FallbackTestSet, t::LogTestFailure)
8383
end
8484

8585
function record(ts::DefaultTestSet, t::LogTestFailure)
86-
if myid() == 1
86+
if TESTSET_PRINT_ENABLE[]
8787
printstyled(ts.description, ": ", color=:white)
8888
print(t)
8989
Base.show_backtrace(stdout, scrub_backtrace(backtrace()))

stdlib/Test/test/runtests.jl

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

3-
using Test, Distributed, Random
3+
using Test, Random
44
using Test: guardseed
5+
using Serialization
6+
using Distributed: RemoteException
57

68
import Logging: Debug, Info, Warn
79

@@ -394,7 +396,7 @@ end
394396
@test total_broken == 0
395397
end
396398
ts.anynonpass = false
397-
deleteat!(Test.get_testset().results,1)
399+
deleteat!(Test.get_testset().results, 1)
398400
end
399401

400402
@test .1+.1+.1 .3
@@ -953,3 +955,17 @@ end
953955
end
954956
@test ok
955957
end
958+
959+
let ex = :(something_complex + [1, 2, 3])
960+
b = PipeBuffer()
961+
let t = Test.Pass(:test, (ex, 1), (ex, 2), (ex, 3))
962+
serialize(b, t)
963+
@test string(t) == string(deserialize(b))
964+
@test eof(b)
965+
end
966+
let t = Test.Broken(:test, ex)
967+
serialize(b, t)
968+
@test string(t) == string(deserialize(b))
969+
@test eof(b)
970+
end
971+
end

test/runtests.jl

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,21 @@ cd(@__DIR__) do
147147
end
148148
end
149149

150-
function print_testworker_errored(name, wrkr)
150+
function print_testworker_errored(name, wrkr, @nospecialize(e))
151151
lock(print_lock)
152152
try
153153
printstyled(name, color=:red)
154154
printstyled(lpad("($wrkr)", name_align - textwidth(name) + 1, " "), " |",
155155
" "^elapsed_align, " failed at $(now())\n", color=:red)
156+
if isa(e, Test.TestSetException)
157+
for t in e.errors_and_fails
158+
show(t)
159+
println()
160+
end
161+
elseif e !== nothing
162+
Base.showerror(stdout, e)
163+
end
164+
println()
156165
finally
157166
unlock(print_lock)
158167
end
@@ -200,18 +209,17 @@ cd(@__DIR__) do
200209
while length(tests) > 0
201210
test = popfirst!(tests)
202211
running_tests[test] = now()
203-
local resp
204212
wrkr = p
205-
try
206-
resp = remotecall_fetch(runtests, wrkr, test, test_path(test); seed=seed)
207-
catch e
208-
isa(e, InterruptException) && return
209-
resp = Any[e]
210-
end
213+
resp = try
214+
remotecall_fetch(runtests, wrkr, test, test_path(test); seed=seed)
215+
catch e
216+
isa(e, InterruptException) && return
217+
Any[CapturedException(e, catch_backtrace())]
218+
end
211219
delete!(running_tests, test)
212220
push!(results, (test, resp))
213-
if resp[1] isa Exception
214-
print_testworker_errored(test, wrkr)
221+
if resp[1] isa Exception && !(resp[1] isa TestSetException && isempty(resp[1].errors_and_fails))
222+
print_testworker_errored(test, wrkr, exit_on_error ? nothing : resp[1])
215223
if exit_on_error
216224
skipped = length(tests)
217225
empty!(tests)
@@ -311,6 +319,7 @@ cd(@__DIR__) do
311319
Errored, and execution continues until the summary at the end of the test
312320
run, where the test file is printed out as the "failed expression".
313321
=#
322+
Test.TESTSET_PRINT_ENABLE[] = false
314323
o_ts = Test.DefaultTestSet("Overall")
315324
Test.push_testset(o_ts)
316325
completed_tests = Set{String}()
@@ -320,29 +329,15 @@ cd(@__DIR__) do
320329
Test.push_testset(resp)
321330
Test.record(o_ts, resp)
322331
Test.pop_testset()
323-
elseif isa(resp, Tuple{Int,Int})
324-
fake = Test.DefaultTestSet(testname)
325-
for i in 1:resp[1]
326-
Test.record(fake, Test.Pass(:test, nothing, nothing, nothing))
327-
end
328-
for i in 1:resp[2]
329-
Test.record(fake, Test.Broken(:test, nothing))
330-
end
331-
Test.push_testset(fake)
332-
Test.record(o_ts, fake)
333-
Test.pop_testset()
334-
elseif isa(resp, RemoteException) && isa(resp.captured.ex, Test.TestSetException)
335-
println("Worker $(resp.pid) failed running test $(testname):")
336-
Base.showerror(stdout, resp.captured)
337-
println()
332+
elseif isa(resp, Test.TestSetException)
338333
fake = Test.DefaultTestSet(testname)
339-
for i in 1:resp.captured.ex.pass
334+
for i in 1:resp.pass
340335
Test.record(fake, Test.Pass(:test, nothing, nothing, nothing))
341336
end
342-
for i in 1:resp.captured.ex.broken
337+
for i in 1:resp.broken
343338
Test.record(fake, Test.Broken(:test, nothing))
344339
end
345-
for t in resp.captured.ex.errors_and_fails
340+
for t in resp.errors_and_fails
346341
Test.record(fake, t)
347342
end
348343
Test.push_testset(fake)
@@ -371,6 +366,7 @@ cd(@__DIR__) do
371366
Test.record(o_ts, fake)
372367
Test.pop_testset()
373368
end
369+
Test.TESTSET_PRINT_ENABLE[] = true
374370
println()
375371
Test.print_test_results(o_ts, 1)
376372
if !o_ts.anynonpass

test/testdefs.jl

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,28 @@ function runtests(name, path, isolate=true; seed=nothing)
1818
let id = myid()
1919
wait(@spawnat 1 print_testworker_started(name, id))
2020
end
21-
ex = quote
22-
@timed @testset $"$name" begin
23-
# Random.seed!(nothing) will fail
24-
$seed != nothing && Random.seed!($seed)
25-
include($"$path.jl")
26-
end
21+
res_and_time_data = @timed @testset "$name" begin
22+
# Random.seed!(nothing) will fail
23+
seed != nothing && Random.seed!(seed)
24+
Base.include(m, "$path.jl")
2725
end
28-
res_and_time_data = Core.eval(m, ex)
2926
rss = Sys.maxrss()
3027
#res_and_time_data[1] is the testset
31-
passes,fails,error,broken,c_passes,c_fails,c_errors,c_broken = Test.get_test_counts(res_and_time_data[1])
32-
if res_and_time_data[1].anynonpass == false
33-
res_and_time_data = (
34-
(passes+c_passes,broken+c_broken),
35-
res_and_time_data[2],
36-
res_and_time_data[3],
37-
res_and_time_data[4],
38-
res_and_time_data[5])
39-
end
40-
vcat(collect(res_and_time_data), rss)
41-
finally
28+
ts = res_and_time_data[1]
29+
passes, fails, errors, broken, c_passes, c_fails, c_errors, c_broken = Test.get_test_counts(ts)
30+
# simplify our stored data to just contain the counts
31+
res_and_time_data = (TestSetException(passes+c_passes, fails+c_fails, errors+c_errors, broken+c_broken, Test.filter_errors(ts)),
32+
res_and_time_data[2],
33+
res_and_time_data[3],
34+
res_and_time_data[4],
35+
res_and_time_data[5],
36+
rss)
37+
return res_and_time_data
38+
catch ex
4239
Test.TESTSET_PRINT_ENABLE[] = old_print_setting
40+
ex isa TestSetException || (ex = CapturedException(ex, catch_backtrace()))
41+
# return this as a value to avoid remotecall from mangling it and discarding half of it
42+
return Any[ex]
4343
end
4444
end
4545

0 commit comments

Comments
 (0)