Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @check macro for non-disable-able @assert #41342

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Build system changes

New library functions
---------------------
* `@check cond [msg]` can be used to check if a condition holds and to error otherwise ([#41342])


* `in!(x, s::AbstractSet)` will return whether `x` is in `s`, and insert `x` in `s` if not.
* The new `Libc.mkfifo` function wraps the `mkfifo` C function on Unix platforms ([#34587]).
Expand Down
65 changes: 65 additions & 0 deletions base/error.jl
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,71 @@ macro assert(ex, msgs...)
return :($(esc(ex)) ? $(nothing) : throw(AssertionError($msg)))
end

# Copied from `macro assert` above for bootstrapping reasons
function prepare_error(ex, msgs...)
msg = isempty(msgs) ? ex : msgs[1]
if isa(msg, AbstractString)
msg = msg # pass-through
elseif !isempty(msgs) && (isa(msg, Expr) || isa(msg, Symbol))
# message is an expression needing evaluating
msg = :(Main.Base.string($(esc(msg))))
elseif isdefined(Main, :Base) && isdefined(Main.Base, :string) && applicable(Main.Base.string, msg)
msg = Main.Base.string(msg)
else
# string() might not be defined during bootstrap
msg = quote
msg = $(Expr(:quote,msg))
isdefined(Main, :Base) ? Main.Base.string(msg) :
(Core.println(msg); "Error during bootstrap. See stdout.")
end
end
return msg
end

"""
CheckError([msg])

The checked condition did not evaluate to `true`.
Optional argument `msg` is a descriptive error string.

# Examples
```jldoctest
julia> @check false "this is not true"
ERROR: CheckError: this is not true
```

`CheckError` is usually thrown from [`@check`](@ref).
"""
struct CheckError <: Exception
msg::AbstractString
end
CheckError() = CheckError("")

"""
@check cond [text]

Throw an [`CheckError`](@ref) if `cond` is `false`.
Message `text` is optionally displayed upon check failure.

Similar to [`@assert`](@ref), except `@check` is never disabled.

# Examples
```jldoctest
julia> @check iseven(3) "3 is an odd number!"
ERROR: CheckError: 3 is an odd number!

julia> @check isodd(3) "What even are numbers?"
```

!!! compat "Julia 1.8"
This macro was added in Julia 1.8.

"""
macro check(ex, msgs...)
msg = prepare_error(ex, msgs...)
return :($(esc(ex)) ? $(nothing) : throw(CheckError($msg)))
end

struct ExponentialBackOff
n::Int
first_delay::Float64
Expand Down
1 change: 1 addition & 0 deletions base/errorshow.jl
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ showerror(io::IO, ex::InterruptException) = print(io, "InterruptException:")
showerror(io::IO, ex::ArgumentError) = print(io, "ArgumentError: ", ex.msg)
showerror(io::IO, ex::DimensionMismatch) = print(io, "DimensionMismatch: ", ex.msg)
showerror(io::IO, ex::AssertionError) = print(io, "AssertionError: ", ex.msg)
showerror(io::IO, ex::CheckError) = print(io, "CheckError: ", ex.msg)
showerror(io::IO, ex::OverflowError) = print(io, "OverflowError: ", ex.msg)

showerror(io::IO, ex::UndefKeywordError) =
Expand Down
2 changes: 2 additions & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export
# Exceptions
CanonicalIndexError,
CapturedException,
CheckError,
CompositeException,
DimensionMismatch,
EOFError,
Expand Down Expand Up @@ -1063,6 +1064,7 @@ export
@polly,

@assert,
@check,
@atomic,
@atomicswap,
@atomicreplace,
Expand Down
2 changes: 2 additions & 0 deletions doc/src/base/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,12 @@ Base.backtrace
Base.catch_backtrace
Base.current_exceptions
Base.@assert
Base.@check
Base.Experimental.register_error_hint
Base.Experimental.show_error_hints
Base.ArgumentError
Base.AssertionError
Base.CheckError
Core.BoundsError
Base.CompositeException
Base.DimensionMismatch
Expand Down
122 changes: 69 additions & 53 deletions test/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,77 @@ include("testhelpers/withlocales.jl")

# Tests that do not really go anywhere else

# test @assert macro
@test_throws AssertionError (@assert 1 == 2)
@test_throws AssertionError (@assert false)
@test_throws AssertionError (@assert false "this is a test")
@test_throws AssertionError (@assert false "this is a test" "another test")
@test_throws AssertionError (@assert false :a)
let
try
@assert 1 == 2
error("unexpected")
catch ex
@test isa(ex, AssertionError)
@test occursin("1 == 2", ex.msg)
end
end
# test @assert message
let
try
@assert 1 == 2 "this is a test"
error("unexpected")
catch ex
@test isa(ex, AssertionError)
@test ex.msg == "this is a test"
end
end
# @assert only uses the first message string
let
try
@assert 1 == 2 "this is a test" "this is another test"
error("unexpected")
catch ex
@test isa(ex, AssertionError)
@test ex.msg == "this is a test"
end
end
# @assert calls string() on second argument
let
try
@assert 1 == 2 :random_object
error("unexpected")
catch ex
@test isa(ex, AssertionError)
@test !occursin("1 == 2", ex.msg)
@test occursin("random_object", ex.msg)
# test @assert and `@check` macro
# These have the same semantics (except `@assert` is allowed
# to be disabled in the future), so we test them with the same tests.
ASSERT_OR_CHECK = Ref(:assert)
macro assert_or_check(expr...)
if ASSERT_OR_CHECK[] == :assert
var"@assert"(__source__, __module__, expr...)
else
var"@check"(__source__, __module__, expr...)
end
end
# if the second argument is an expression, c
let deepthought(x, y) = 42
try
@assert 1 == 2 string("the answer to the ultimate question: ",
deepthought(6, 9))
error("unexpected")
catch ex
@test isa(ex, AssertionError)
@test ex.msg == "the answer to the ultimate question: 42"
@testset "@$(val)" for val in (:assert, :check)
ASSERT_OR_CHECK[] = val
@eval begin # eval to delay macro expansion until after we've assigned `val`
E = ASSERT_OR_CHECK[] == :assert ? AssertionError : CheckError
@test_throws E (@assert_or_check 1 == 2)
@test_throws E (@assert_or_check false)
@test_throws E (@assert_or_check false "this is a test")
@test_throws E (@assert_or_check false "this is a test" "another test")
@test_throws E (@assert_or_check false :a)
let
try
@assert_or_check 1 == 2
error("unexpected")
catch ex
@test isa(ex, E)
@test occursin("1 == 2", ex.msg)
end
end
# test the macro's message
let
try
@assert_or_check 1 == 2 "this is a test"
error("unexpected")
catch ex
@test isa(ex, E)
@test ex.msg == "this is a test"
end
end
# the macro only uses the first message string
let
try
@assert_or_check 1 == 2 "this is a test" "this is another test"
error("unexpected")
catch ex
@test isa(ex, E)
@test ex.msg == "this is a test"
end
end
# the macro calls string() on second argument
let
try
@assert_or_check 1 == 2 :random_object
error("unexpected")
catch ex
@test isa(ex, E)
@test !occursin("1 == 2", ex.msg)
@test occursin("random_object", ex.msg)
end
end
# if the second argument is an expression, c
let deepthought(x, y) = 42
try
@assert_or_check 1 == 2 string("the answer to the ultimate question: ",
deepthought(6, 9))
error("unexpected")
catch ex
@test isa(ex, E)
@test ex.msg == "the answer to the ultimate question: 42"
end
end
end
end

Expand Down