@test_throws UndefVarError f(x::A)=x

With a fresh Julia session:

julia> using Test

julia> @test_throws UndefVarError foo(x::A)=x
ERROR: UndefVarError: `A` not defined

Should this be a passing test? I don’t quite get why the UndefVarError escapes the @test_throws.

(My actual code is much more complicated and involves a macro I wrote, but I think my issue boils down to this example.)

This throws the exception before the test actually runs.

I think you would need to use eval to get the function to be defined when the test is being run

Well, that’s fairly obvious. :wink: But I don’t really understand why—the order of operations is not what I would expect.

The expression foo(x::A) = x shouldn’t throw an UndefVarError until the expression is actually evaluated. For example, this does not throw an error:

julia> if false
           foo(x::A) = x
       else
           1
       end
1

Interestingly, this leads to an even more perverse example with @test_throws:

julia> @test_throws UndefVarError begin
           if false
               foo(x::A) = x
           else
               1
           end
       end
ERROR: UndefVarError: `A` not defined

Or a similar example with @test:

julia> @test begin
           if false
               foo(x::A) = x
               true
           else
               true
           end
       end
ERROR: UndefVarError: `A` not defined

My understanding is that the @test_throws macro should expand before foo(x::A) = x is evaluated, and the @test_throws macro expands to some kind of try-catch block. Note that a basic try-catch block works as expected:

julia> try
           foo(x::A) = x
       catch
           println("hello world")
       end
hello world

So, let’s take a closer look at what @test_throws expands to. After re-typesetting the output of @macroexpand in my editor to make it more readable, the output of @macroexpand @test_throws UndefVarError f(x::A)=x is

quote
    Test.do_test_throws(
        begin
            try
                Test.Returned(
                    $(Expr(:(=), :(foo(x::A)), quote x end)),
                    Test.nothing,
                    $(QuoteNode(:(#= REPL[8]:1 =#)))
                )
            catch var"#24#_e"
                if UndefVarError != Test.InterruptException && var"#24#_e" isa Test.InterruptException
                    Test.rethrow()
                end
                Test.Threw(var"#24#_e", Test.nothing, $(QuoteNode(:(#= REPL[8]:1 =#))))
            end
        end,
        $(QuoteNode(:(foo(x::A) = begin
            x
        end))),
        UndefVarError
    )
end

But I don’t know enough about Test internals to be able to interpret that…

Ah, this is because it’s creating a closure, even with the named function syntax, as it uses a local scope. And closures implicitly create structs ahead of time. In other words, the @macroexpand is ok, but the problem comes at the next step, in lowering and you can see the problem (albeit hard to read) with Meta.@lower.

Or perhaps more clearly, this function errors upon definition, not upon execution:

julia> function test()
           return (::not_defined) -> 2
       end
ERROR: UndefVarError: `not_defined` not defined

And similarly, this more clearly shows that the problem is happening at definition-time:

julia> function test()
           @test_throws UndefVarError (::not_defined) -> 2
       end
ERROR: UndefVarError: `not_defined` not defined

If you were to do this with something that’s not a closure, it works as you expect:

julia> @test_throws UndefVarError Base.sum(::not_defined) = 2
Test Passed
      Thrown: UndefVarError
1 Like