diff --git a/src/macros.jl b/src/macros.jl index a8dab61..3cfd974 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -74,18 +74,47 @@ macro test_trixi_include_base(elixir, args...) local atol = get_kwarg(args, :atol, atol_default) local rtol = get_kwarg(args, :rtol, rtol_default) - local kwargs = Pair{Symbol, Any}[] + # Build escaped kwarg expressions. For bare-symbol values there are three cases: + # 1. Symbol is also a key in this kwarg list (e.g. `seed=6, x=seed`): always pass + # as Symbol so trixi_include resolves it inside the elixir after the other + # override (seed=6) has been applied. + # 2. Locally-defined values defined in the testset body: + # @isdefined returns true (same world age), so the actual value is passed. + # 3. Elixir-internal variable references (e.g. bare `x=seed` with no seed= key): + # on Julia >= 1.12, @isdefined returns false because bindings set inside + # Base.include have a newer world age; on older Julia the value is visible and + # also correct (same as the elixir default). + # For non-Symbol expressions (closures, calls, literals), always evaluate at call site. + local kwarg_keys = Set(arg.args[1] + for arg in args + if arg.head == :(=) && + !(arg.args[1] in (:additional_ignore_content, :l2, :linf, + :RealT_for_test_tolerances, :atol, :rtol))) + local kwarg_exprs = Expr[] for arg in args if (arg.head == :(=) && !(arg.args[1] in (:additional_ignore_content, :l2, :linf, :RealT_for_test_tolerances, :atol, :rtol))) - push!(kwargs, Pair(arg.args...)) + key = arg.args[1] + val = arg.args[2] + if val isa Symbol && val in kwarg_keys + # Case 1: chained override — pass as Symbol for elixir-internal resolution + push!(kwarg_exprs, Expr(:kw, key, QuoteNode(val))) + elseif val isa Symbol + # Cases 2 & 3: use @isdefined to capture locally-defined values while + # falling back to Symbol for elixir-internal references + push!(kwarg_exprs, + Expr(:kw, key, + esc(:((@isdefined $val) ? $val : $(QuoteNode(val)))))) + else + push!(kwarg_exprs, Expr(:kw, key, esc(val))) + end end end # if `maxiters` is set in tests, it is usually set to a small number to # run only a few steps - ignore possible warnings coming from that - if any(==(:maxiters) ∘ first, kwargs) + if any(e -> e.args[1] == :maxiters, kwarg_exprs) args = append_to_kwargs(args, :additional_ignore_content, [ r"┌ Warning: Verbosity toggle: max_iters \n│ Interrupted\. Larger maxiters is needed\..*\n└ @ SciMLBase .+\n", @@ -99,7 +128,7 @@ macro test_trixi_include_base(elixir, args...) mpi_isroot() && println($(esc(elixir))) # evaluate examples in the scope of the module they're called from - @trixi_test_nowarn trixi_include(@__MODULE__, $(esc(elixir)); $kwargs...) $additional_ignore_content + @trixi_test_nowarn trixi_include(@__MODULE__, $(esc(elixir)); $(kwarg_exprs...)) $additional_ignore_content # if present, compare l2 and linf errors against reference values if !isnothing($l2) || !isnothing($linf) diff --git a/test/test_test_trixi_include.jl b/test/test_test_trixi_include.jl index bc3e22a..a8ebe5d 100644 --- a/test/test_test_trixi_include.jl +++ b/test/test_test_trixi_include.jl @@ -20,45 +20,25 @@ end # just include @test_trixi_include_base(path) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 4 - else - @test @isdefined x - @test x == 4 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 4 @test_trixi_include(path) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 4 - else - @test @isdefined x - @test x == 4 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 4 # include and overwrite included variable by a constant @test_trixi_include_base(path, x=9) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 9 - else - @test @isdefined x - @test x == 9 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 9 @test_trixi_include(path, x=9) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 9 - else - @test @isdefined x - @test x == 9 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 9 end end @@ -75,66 +55,159 @@ end # overwrite included variable by a (global) variable global override = 5 @test_trixi_include_base(path, x=override) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 5 - else - @test @isdefined x - @test x == 5 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 5 @test_trixi_include(path, x=override) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 5 - else - @test @isdefined x - @test x == 5 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 5 # overwrite included variable by another included variable @test_trixi_include_base(path, x=seed) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 42 - else - @test @isdefined x - @test x == 42 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 42 @test_trixi_include(path, x=seed) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 42 - else - @test @isdefined x - @test x == 42 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 42 # overwrite included variable by supplied variable @test_trixi_include_base(path, seed=6, x=seed) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 6 - else - @test @isdefined x - @test x == 6 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 6 @test_trixi_include(path, seed=6, x=seed) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :x) - @test (@invokelatest mod.x) == 6 - else - @test @isdefined x - @test x == 6 + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :x) + @test (@invokelatest mod.x) == 6 + end + end + + @trixi_testset "normal override, all assignment forms" begin + global f(; x = 0) = x + example = """ + x = 1 + x_kw_pos = f(x = 1) + x_kw_semi = f(; x = 1) + y = (; x = 1) + function g(; x = 1) + return x end + y_g = g() + function h(x = 1) + return x + end + y_h = h() + y_let = 0 + y_let_global = 0 + let x = 1 + y_let = x + global y_let_global = x + end + """ + + mktemp() do path, io + write(io, example) + close(io) + + @test_trixi_include_base(path, x=6) + mod = @__MODULE__ + @test (@invokelatest mod.x) == 6 + @test (@invokelatest mod.x_kw_pos) == 6 + @test (@invokelatest mod.x_kw_semi) == 6 + @test (@invokelatest mod.y).x == 6 + @test (@invokelatest mod.y_g) == 6 + @test (@invokelatest mod.y_h) == 6 + @test (@invokelatest mod.y_let) == 0 # let block introduces a local scope + @test (@invokelatest mod.y_let_global) == 6 + end + end + + @trixi_testset "chained override, all assignment forms" begin + global f(; x = 0) = x + example = """ + seed = 42 + x = 1 + x_kw_pos = f(x = 1) + x_kw_semi = f(; x = 1) + y = (; x = 1) + function g(; x = 1) + return x + end + y_g = g() + function h(x = 1) + return x + end + y_h = h() + y_let = 0 + y_let_global = 0 + let x = 1 + y_let = x + global y_let_global = x + end + """ + + mktemp() do path, io + write(io, example) + close(io) + + @test_trixi_include_base(path, seed=6, x=seed) + mod = @__MODULE__ + @test (@invokelatest mod.x) == 6 + @test (@invokelatest mod.x_kw_pos) == 6 + @test (@invokelatest mod.x_kw_semi) == 6 + @test (@invokelatest mod.y).x == 6 + @test (@invokelatest mod.y_g) == 6 + @test (@invokelatest mod.y_h) == 6 + @test (@invokelatest mod.y_let) == 0 # let block introduces a local scope + @test (@invokelatest mod.y_let_global) == 6 + end + end + + @trixi_testset "locally defined override, all assignment forms" begin + global f(; x = 0) = x + example = """ + x = 1 + x_kw_pos = f(x = 1) + x_kw_semi = f(; x = 1) + y = (; x = 1) + function g(; x = 1) + return x + end + y_g = g() + function h(x = 1) + return x + end + y_h = h() + y_let = 0 + y_let_global = 0 + let x = 1 + y_let = x + global y_let_global = x + end + """ + + mktemp() do path, io + write(io, example) + close(io) + + # overwrite included variable by a locally defined value (not a module global) + local_x = 6 + @test_trixi_include_base(path, x=local_x) + mod = @__MODULE__ + @test (@invokelatest mod.x) == 6 + @test (@invokelatest mod.x_kw_pos) == 6 + @test (@invokelatest mod.x_kw_semi) == 6 + @test (@invokelatest mod.y).x == 6 + @test (@invokelatest mod.y_g) == 6 + @test (@invokelatest mod.y_h) == 6 + @test (@invokelatest mod.y_let) == 0 # let block introduces a local scope + @test (@invokelatest mod.y_let_global) == 6 end end @@ -236,24 +309,14 @@ end close(io) @test_trixi_include_base(path, RealT=Float32) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :RealT) - @test (@invokelatest mod.RealT) == Float32 - else - @test @isdefined RealT - @test RealT == Float32 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :RealT) + @test (@invokelatest mod.RealT) == Float32 @test_trixi_include(path, RealT=Float32) - if VERSION >= v"1.12" - mod = @__MODULE__ - @test @invokelatest isdefined(mod, :RealT) - @test (@invokelatest mod.RealT) == Float32 - else - @test @isdefined RealT - @test RealT == Float32 - end + mod = @__MODULE__ + @test @invokelatest isdefined(mod, :RealT) + @test (@invokelatest mod.RealT) == Float32 end end end