diff --git a/src/macros.jl b/src/macros.jl index 3cfd974..02e90fb 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -84,7 +84,19 @@ macro test_trixi_include_base(elixir, args...) # 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. + # For non-Symbol expressions, there are two cases: + # 4. Literals (numbers, strings, ...): the value is the same regardless of the + # scope, so we can simply pass it on. + # 5. Compound expressions (calls, tuples, array/closure literals, e.g. + # `surface_flux=FluxLaxFriedrichs(max_abs_speed)`): these typically reference + # names that are only available *inside* the elixir's scope (e.g. brought in by + # the elixir's own `using Trixi`) and are not defined at the macro call site + # (the testset module). Hence, we must NOT evaluate them at the call site but + # pass the unevaluated expression through to `trixi_include`, which splices it + # into the elixir and evaluates it in the elixir's scope. + # We achieve both of the above by passing the (quoted) expression on unevaluated via + # a `QuoteNode`, which reproduces the behavior before locally-defined Symbol values + # were supported. local kwarg_keys = Set(arg.args[1] for arg in args if arg.head == :(=) && @@ -107,7 +119,9 @@ macro test_trixi_include_base(elixir, args...) Expr(:kw, key, esc(:((@isdefined $val) ? $val : $(QuoteNode(val)))))) else - push!(kwarg_exprs, Expr(:kw, key, esc(val))) + # Cases 4 & 5: pass the unevaluated expression through to + # `trixi_include` for resolution in the elixir's scope + push!(kwarg_exprs, Expr(:kw, key, QuoteNode(val))) end end end diff --git a/test/Project.toml b/test/Project.toml index cb6f3d5..87db028 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,9 +1,11 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TrixiBase = "9a0f1c46-06d5-4909-a5a3-ce25d3fa3284" [compat] Aqua = "0.8" +LinearAlgebra = "1" Test = "1" TrixiBase = "0.1.6" diff --git a/test/test_test_trixi_include.jl b/test/test_test_trixi_include.jl index a8ebe5d..b57d6ca 100644 --- a/test/test_test_trixi_include.jl +++ b/test/test_test_trixi_include.jl @@ -211,6 +211,67 @@ end end end + @trixi_testset "compound (non-Symbol) override values" begin + # Regression test for a bug where compound kwarg values (e.g. function + # calls) were evaluated in the testset module from which the macro is + # called instead of in the elixir's scope. This broke values referencing + # names that are only available *inside* the elixir, such as + # `surface_flux=FluxLaxFriedrichs(max_abs_speed)` in Trixi.jl, where + # `FluxLaxFriedrichs` and `max_abs_speed` come from the elixir's own + # `using Trixi` and are not defined in the (inner) testset module. + # + # We mimic this here with `norm` from `LinearAlgebra`: the elixir brings + # it in via its own `using LinearAlgebra`, while the testset module from + # which the macros are called below does *not* have `LinearAlgebra` (and + # `norm` is therefore not defined there). + example = """ + using LinearAlgebra + x = norm([3.0, 4.0]) + t = (1, 2) + s = "default" + """ + + mktemp() do path, io + write(io, example) + close(io) + mod = @__MODULE__ + + # `norm` is intentionally not available in this testset module, so + # evaluating the override value here (instead of in the elixir's + # scope) would throw an `UndefVarError`. + @test !(@invokelatest isdefined(mod, :norm)) + + # Compound call expression referencing an elixir-internal name + @test_trixi_include_base(path, x=norm([6.0, 8.0])) + @test (@invokelatest mod.x) ≈ 10.0 + + @test_trixi_include(path, x=norm([6.0, 8.0])) + @test (@invokelatest mod.x) ≈ 10.0 + + # Tuple expression + @test_trixi_include_base(path, t=(3, 4)) + @test (@invokelatest mod.t) == (3, 4) + + @test_trixi_include(path, t=(3, 4)) + @test (@invokelatest mod.t) == (3, 4) + + # String literal + @test_trixi_include_base(path, s="override") + @test (@invokelatest mod.s) == "override" + + # Numeric literal still works through the same code path + @test_trixi_include_base(path, x=7) + @test (@invokelatest mod.x) == 7 + + # Combining a compound override with a chained Symbol override: + # `t=(x, x)` must be resolved in the elixir's scope *after* the + # `x` override has been applied there. + @test_trixi_include_base(path, x=norm([5.0, 12.0]), t=(x, x)) + @test (@invokelatest mod.x) ≈ 13.0 + @test all((@invokelatest mod.t) .≈ (13.0, 13.0)) + end + end + @trixi_testset "additional_ignore_content" begin example = """ @warn "Test warning"