Skip to content

Commit 96de59e

Browse files
timholyLilithHafner
authored andcommitted
Support @test_throws just checking the error message (JuliaLang#41888)
With this PR, @test_throws "reducing over an empty collection" reduce(+, ()) allows you to check the displayed error message without caring about the details of which specific Exception subtype is used. The pattern-matching options are the same as for `@test_warn`. Co-authored by: Jameson Nash <[email protected]> Co-authored by: Takafumi Arakaki <[email protected]>
1 parent f97aabd commit 96de59e

File tree

3 files changed

+91
-14
lines changed

3 files changed

+91
-14
lines changed

NEWS.md

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ New library functions
3737
New library features
3838
--------------------
3939

40+
* `@test_throws "some message" triggers_error()` can now be used to check whether the displayed error text
41+
contains "some message" regardless of the specific exception type.
42+
Regular expressions, lists of strings, and matching functions are also supported. ([#41888)
4043

4144
Standard library changes
4245
------------------------

stdlib/Test/src/Test.jl

+51-13
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ struct Pass <: Result
8686
data
8787
value
8888
source::Union{Nothing,LineNumberNode}
89-
function Pass(test_type::Symbol, orig_expr, data, thrown, source=nothing)
90-
return new(test_type, orig_expr, data, thrown isa String ? "String" : thrown, source)
89+
message_only::Bool
90+
function Pass(test_type::Symbol, orig_expr, data, thrown, source=nothing, message_only=false)
91+
return new(test_type, orig_expr, data, thrown, source, message_only)
9192
end
9293
end
9394

@@ -98,7 +99,11 @@ function Base.show(io::IO, t::Pass)
9899
end
99100
if t.test_type === :test_throws
100101
# The correct type of exception was thrown
101-
print(io, "\n Thrown: ", t.value isa String ? t.value : typeof(t.value))
102+
if t.message_only
103+
print(io, "\n Message: ", t.value)
104+
else
105+
print(io, "\n Thrown: ", typeof(t.value))
106+
end
102107
elseif t.test_type === :test && t.data !== nothing
103108
# The test was an expression, so display the term-by-term
104109
# evaluated version as well
@@ -118,12 +123,14 @@ struct Fail <: Result
118123
data::Union{Nothing, String}
119124
value::String
120125
source::LineNumberNode
121-
function Fail(test_type::Symbol, orig_expr, data, value, source::LineNumberNode)
126+
message_only::Bool
127+
function Fail(test_type::Symbol, orig_expr, data, value, source::LineNumberNode, message_only::Bool=false)
122128
return new(test_type,
123129
string(orig_expr),
124130
data === nothing ? nothing : string(data),
125131
string(isa(data, Type) ? typeof(value) : value),
126-
source)
132+
source,
133+
message_only)
127134
end
128135
end
129136

@@ -132,18 +139,24 @@ function Base.show(io::IO, t::Fail)
132139
print(io, " at ")
133140
printstyled(io, something(t.source.file, :none), ":", t.source.line, "\n"; bold=true, color=:default)
134141
print(io, " Expression: ", t.orig_expr)
142+
value, data = t.value, t.data
135143
if t.test_type === :test_throws_wrong
136144
# An exception was thrown, but it was of the wrong type
137-
print(io, "\n Expected: ", t.data)
138-
print(io, "\n Thrown: ", t.value)
145+
if t.message_only
146+
print(io, "\n Expected: ", data)
147+
print(io, "\n Message: ", value)
148+
else
149+
print(io, "\n Expected: ", data)
150+
print(io, "\n Thrown: ", value)
151+
end
139152
elseif t.test_type === :test_throws_nothing
140153
# An exception was expected, but no exception was thrown
141-
print(io, "\n Expected: ", t.data)
154+
print(io, "\n Expected: ", data)
142155
print(io, "\n No exception thrown")
143-
elseif t.test_type === :test && t.data !== nothing
156+
elseif t.test_type === :test && data !== nothing
144157
# The test was an expression, so display the term-by-term
145158
# evaluated version as well
146-
print(io, "\n Evaluated: ", t.data)
159+
print(io, "\n Evaluated: ", data)
147160
end
148161
end
149162

@@ -238,6 +251,7 @@ function Serialization.serialize(s::Serialization.AbstractSerializer, t::Pass)
238251
Serialization.serialize(s, t.data === nothing ? nothing : string(t.data))
239252
Serialization.serialize(s, string(t.value))
240253
Serialization.serialize(s, t.source === nothing ? nothing : t.source)
254+
Serialization.serialize(s, t.message_only)
241255
nothing
242256
end
243257

@@ -657,6 +671,8 @@ end
657671
658672
Tests that the expression `expr` throws `exception`.
659673
The exception may specify either a type,
674+
a string, regular expression, or list of strings occurring in the displayed error message,
675+
a matching function,
660676
or a value (which will be tested for equality by comparing fields).
661677
Note that `@test_throws` does not support a trailing keyword form.
662678
@@ -671,7 +687,18 @@ julia> @test_throws DimensionMismatch [1, 2, 3] + [1, 2]
671687
Test Passed
672688
Expression: [1, 2, 3] + [1, 2]
673689
Thrown: DimensionMismatch
690+
691+
julia> @test_throws "Try sqrt(Complex" sqrt(-1)
692+
Test Passed
693+
Expression: sqrt(-1)
694+
Message: "DomainError with -1.0:\\nsqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x))."
674695
```
696+
697+
In the final example, instead of matching a single string it could alternatively have been performed with:
698+
699+
- `["Try", "Complex"]` (a list of strings)
700+
- `r"Try sqrt\\([Cc]omplex"` (a regular expression)
701+
- `str -> occursin("complex", str)` (a matching function)
675702
"""
676703
macro test_throws(extype, ex)
677704
orig_ex = Expr(:inert, ex)
@@ -697,6 +724,7 @@ function do_test_throws(result::ExecutionResult, orig_expr, extype)
697724
if isa(result, Threw)
698725
# Check that the right type of exception was thrown
699726
success = false
727+
message_only = false
700728
exc = result.exception
701729
# NB: Throwing LoadError from macroexpands is deprecated, but in order to limit
702730
# the breakage in package tests we add extra logic here.
@@ -712,7 +740,7 @@ function do_test_throws(result::ExecutionResult, orig_expr, extype)
712740
else
713741
isa(exc, extype)
714742
end
715-
else
743+
elseif isa(extype, Exception) || !isa(exc, Exception)
716744
if extype isa LoadError && !(exc isa LoadError) && typeof(extype.error) == typeof(exc)
717745
extype = extype.error # deprecated
718746
end
@@ -725,11 +753,21 @@ function do_test_throws(result::ExecutionResult, orig_expr, extype)
725753
end
726754
end
727755
end
756+
else
757+
message_only = true
758+
exc = sprint(showerror, exc)
759+
success = contains_warn(exc, extype)
760+
exc = repr(exc)
761+
if isa(extype, AbstractString)
762+
extype = repr(extype)
763+
elseif isa(extype, Function)
764+
extype = "< match function >"
765+
end
728766
end
729767
if success
730-
testres = Pass(:test_throws, orig_expr, extype, exc, result.source)
768+
testres = Pass(:test_throws, orig_expr, extype, exc, result.source, message_only)
731769
else
732-
testres = Fail(:test_throws_wrong, orig_expr, extype, exc, result.source)
770+
testres = Fail(:test_throws_wrong, orig_expr, extype, exc, result.source, message_only)
733771
end
734772
else
735773
testres = Fail(:test_throws_nothing, orig_expr, extype, nothing, result.source)

stdlib/Test/test/runtests.jl

+37-1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ end
9696
"Thrown: ErrorException")
9797
@test endswith(sprint(show, @test_throws ErrorException("test") error("test")),
9898
"Thrown: ErrorException")
99+
@test endswith(sprint(show, @test_throws "a test" error("a test")),
100+
"Message: \"a test\"")
101+
@test occursin("Message: \"DomainError",
102+
sprint(show, @test_throws r"sqrt\([Cc]omplex" sqrt(-1)))
103+
@test endswith(sprint(show, @test_throws str->occursin("a t", str) error("a test")),
104+
"Message: \"a test\"")
105+
@test endswith(sprint(show, @test_throws ["BoundsError", "access", "1-element", "at index [2]"] [1][2]),
106+
"Message: \"BoundsError: attempt to access 1-element Vector{$Int} at index [2]\"")
107+
@test_throws "\"" throw("\"")
108+
@test_throws Returns(false) throw(Returns(false))
99109
end
100110
# Test printing of Fail results
101111
include("nothrow_testset.jl")
@@ -148,6 +158,11 @@ let fails = @testset NoThrowTestSet begin
148158
@test contains(str1, str2)
149159
# 22 - Fail - Type Comparison
150160
@test typeof(1) <: typeof("julia")
161+
# 23 - 26 - Fail - wrong message
162+
@test_throws "A test" error("a test")
163+
@test_throws r"sqrt\([Cc]omplx" sqrt(-1)
164+
@test_throws str->occursin("a T", str) error("a test")
165+
@test_throws ["BoundsError", "acess", "1-element", "at index [2]"] [1][2]
151166
end
152167
for fail in fails
153168
@test fail isa Test.Fail
@@ -262,6 +277,27 @@ let fails = @testset NoThrowTestSet begin
262277
@test occursin("Expression: typeof(1) <: typeof(\"julia\")", str)
263278
@test occursin("Evaluated: $(typeof(1)) <: $(typeof("julia"))", str)
264279
end
280+
281+
let str = sprint(show, fails[23])
282+
@test occursin("Expected: \"A test\"", str)
283+
@test occursin("Message: \"a test\"", str)
284+
end
285+
286+
let str = sprint(show, fails[24])
287+
@test occursin("Expected: r\"sqrt\\([Cc]omplx\"", str)
288+
@test occursin(r"Message: .*Try sqrt\(Complex", str)
289+
end
290+
291+
let str = sprint(show, fails[25])
292+
@test occursin("Expected: < match function >", str)
293+
@test occursin("Message: \"a test\"", str)
294+
end
295+
296+
let str = sprint(show, fails[26])
297+
@test occursin("Expected: [\"BoundsError\", \"acess\", \"1-element\", \"at index [2]\"]", str)
298+
@test occursin(r"Message: \"BoundsError.* 1-element.*at index \[2\]", str)
299+
end
300+
265301
end
266302

267303
let errors = @testset NoThrowTestSet begin
@@ -1202,4 +1238,4 @@ Test.finish(ts::PassInformationTestSet) = ts
12021238
@test ts.results[2].data == ErrorException
12031239
@test ts.results[2].value == ErrorException("Msg")
12041240
@test ts.results[2].source == LineNumberNode(test_throws_line_number, @__FILE__)
1205-
end
1241+
end

0 commit comments

Comments
 (0)