Hi, is there any way to invalidate the code of a macro? I have implemented a macro that depends on system environment. Now I’m trying to write a unit test for it. I’m able to delay macro code expansion by placing the first call inside eval/parseall. But I can’t force Julia to recompile the macro for the second state of the DEBUG_OUTPUT check.
Minimized code:
macro zc_get_id(title)
return haskey(ENV, "DEBUG_OUTPUT") ?
:(get_debug_id($(esc(title)))) :
:("")
end
Unit test now:
@testset "zero cost disabled" begin
ENV["DEBUG_OUTPUT"] = "log | dump"
eval(Meta.parseall("""
id = @zc_get_id "test"
@test !isempty(id)
"""))
end
@testset "zero cost enabled" begin
delete!(ENV, "DEBUG_OUTPUT")
eval(Meta.parseall("""
id = @zc_get_id "test"
@test isempty(id)
"""))
end
Inside the second eval, I have already compiled @zc_get_id for the case with the environment variable.
One of the workarounds I see - run a separate julia process for testing each branch of the macro. But not sure if this is a good idea.
(I’ve moved the topic to the General category since this is not a question about julias internals or its design)
As macros run at parse time, i.e. before the "DEBUG_OUTPUT" entry is deleted from ENV, it will always expand based on what ENV contains when the macro is expanded.
In general, I’d advise against making your macro dependent on such fickle global state; consider using a function that returns the expression in question instead and testing whether the returned expression is as you expect it to be, passing the environment variable into the function as an argument. That is much easier to test, while keeping the macro itself free from the global state.
As macros run at parse time, i.e. before the "DEBUG_OUTPUT" entry is deleted from ENV, it will always expand based on what ENV contains when the macro is expanded.
Therefore I separated changing of this variable and analysis/compiling with eval/parseall. But even after running parseall for the second time, I’m getting:
The reason why I decided to create this macro - be able to completely exclude from the prod-code any additional components.
The first variable/function based implementation is looking like:
using DebugDataWriter
# Enable adding trace info with the @info macro
# Each record contains links to the source code and to the saved data file
DebugDataWriter.config().enable_log = true
# Enable saving dumps of data structures
DebugDataWriter.config().enable_dump = true
id = get_debug_id("Some query")
@debug_output id "some complex structure" ones(2, 3)
@debug_output id "some complex structure" ones(2, 3) :HTML
@debug_output id "some complex structure" ones(2, 3) :TXT
# the lambda is executed when enable_dump == true
@debug_output id "some data structure as a lambda" begin
zeros(5, 2)
end
But the idea is to completely remove any additional checks. And this is really work but no idea how to cover it by a unit test.
E.g. a zero cost macro wrapper @zc_dout looks like:
What could be happening is that the macro runs in a different world age when run through your eval/parse combo, but again, that’s best avoided (especially when dealing with macros).
Like I said above - if the environment variable is not defined in your production system, that macro should do what you intend it to do. For unit testing purposes, a construct like this (extracting the work the macro does into a function) should work, where you then test the output of that function:
julia> macro zc_get_id(title)
macro_func(title)
end
@zc_get_id (macro with 1 method)
julia> function macro_func(title)
return haskey(ENV, "DEBUG_OUTPUT") ?
:(get_debug_id($(esc(title)))) :
:("")
end
macro_func (generic function with 1 method)
julia> using Test
julia> @testset "all tests" begin
@testset "zero cost disabled" begin
ENV["DEBUG_OUTPUT"] = "log | dump"
@test macro_func("test") != ""
end
@testset "zero cost enabled" begin
delete!(ENV, "DEBUG_OUTPUT")
@test macro_func("test") == ""
end
end;
Test Summary: | Pass Total Time
all tests | 2 2 0.0s
The macro will still run at parse-time just the same:
Ok, thanks, I found my mistake. Looks when I did a series of experiments, I mixed "" and nothing values. But here I shared the minimized code and lost this error.
At the same time, the following code is not working. I think because of the code of @zc_get_id was expanded before the assignment of the ENV.
@testset "check" begin
ENV["DEBUG_OUTPUT"] = "log | dump"
id = @zc_get_id "boo"
@test !isempty(id)
delete!(ENV, "DEBUG_OUTPUT")
id = @zc_get_id "goo"
@test isempty(id)
end
But this works with eval/parseall.
Regarding the macro_func as a function, this is a good idea. The only problem is that the macro itself is not covered by a unit test. So either macro_func and a typical set of test checks, or eval/parse with running the macro inside …
I’d advise you to use the same trick as ToggleableAsserts.jl, which I think originates from this discourse thread:
With this technique you explicitly toggle the behavior of your macro by redefining a function, which invalidates all code using the macro and forces a recompilation.
ToggleableAsserts show how easily such macros can be tested: