Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/TrixiTest.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ using TrixiBase: mpi_isroot, trixi_include
include("auxiliary.jl")
include("macros.jl")

export get_kwarg, append_to_kwargs
export get_kwarg, append_to_kwargs, trixi_include_kwargs
export @trixi_test_nowarn, @test_trixi_include_base, @timed_testset, @trixi_testset,
@test_allocations

Expand Down
77 changes: 77 additions & 0 deletions src/auxiliary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,80 @@ function append_to_kwargs(args, keyword, value)
end
return args
end

"""
trixi_include_kwargs(args; reserved = ())

Turn the arguments `args` of a testing macro (a tuple of `:(key = value)` expressions, as
they are received by a macro) into a vector of `Expr(:kw, ...)` expressions that can be
spliced into a `trixi_include` call inside the expression the macro returns:
Comment thread
JoshuaLampert marked this conversation as resolved.
Outdated

```julia
macro my_test_include(example, args...)
local kwargs = trixi_include_kwargs(args; reserved = (:l2, :linf))
quote
@trixi_test_nowarn trixi_include(@__MODULE__, \$example; \$(kwargs...))
Comment thread
JoshuaLampert marked this conversation as resolved.
Outdated
end
Comment thread
JoshuaLampert marked this conversation as resolved.
end
```

Keys listed in `reserved` are consumed by the calling macro itself and are skipped, i.e.,
they are not forwarded to `trixi_include`.

This is used by [`@test_trixi_include_base`](@ref) and encapsulates how the unevaluated
keyword arguments of a testing macro have to be passed on to `trixi_include`.

For bare-symbol values there are three cases:
1. Symbol is also a key in this kwarg list (e.g. `seed=6, x=seed`): pass an expression
so that `trixi_include` resolves it inside the example 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. Example-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 example default).
Example-internal bare symbols are wrapped in a block expression. This preserves
example-scope evaluation without passing a `Symbol` *value* to `trixi_include`, which
treats a `Symbol` as a literal value.

For non-Symbol expressions, there are two cases:
4. Literals (numbers, strings, quoted Symbols, ...): the value is the same regardless of
the scope, so we can simply pass it on. Note that a quoted symbol literal such as
`x=:foo` is parsed as a `QuoteNode`, not as a `Symbol`, and hence lands here and is
passed on as the value `:foo`.
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 example's scope (e.g. brought in by the example'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 example and evaluates it in the
example's scope.
Both of the latter are achieved by passing the expression on unevaluated via a `QuoteNode`.
"""
function trixi_include_kwargs(args; reserved = ())
is_forwarded(arg) = arg.head == :(=) && !(arg.args[1] in reserved)

kwarg_keys = Set(arg.args[1] for arg in args if is_forwarded(arg))
kwarg_exprs = Expr[]
for arg in args
is_forwarded(arg) || continue
key, val = arg.args
if val isa Symbol && val in kwarg_keys
# Case 1: chained override — resolve the reference in the example
push!(kwarg_exprs, Expr(:kw, key, QuoteNode(Expr(:block, val))))
elseif val isa Symbol
# Cases 2 & 3: use @isdefined to capture locally-defined values while
# falling back to an expression that resolves example-internal references
# in the example
push!(kwarg_exprs,
Expr(:kw, key,
esc(:((@isdefined $val) ? $val :
$(QuoteNode(Expr(:block, val)))))))
else
# Cases 4 & 5: pass literals and compound expressions on unevaluated
push!(kwarg_exprs, Expr(:kw, key, QuoteNode(val)))
end
end
return kwarg_exprs
end
65 changes: 8 additions & 57 deletions src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,63 +74,14 @@ macro test_trixi_include_base(elixir, args...)
local atol = get_kwarg(args, :atol, atol_default)
local rtol = get_kwarg(args, :rtol, rtol_default)

# 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`): pass an
# expression so that 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).
# Elixir-internal bare symbols are wrapped in a block expression. This preserves
# elixir-scope evaluation without passing a Symbol *value* to trixi_include, which
# treats a Symbol as a literal value.
# For non-Symbol expressions, there are two cases:
# 4. Literals (numbers, strings, quoted Symbols, ...): the value is the same
# regardless of the scope, so we can simply pass it on. Note that a quoted
# symbol literal such as `x=:foo` is parsed as a `QuoteNode`, not as a
# `Symbol`, and hence lands here and is passed on as the value `:foo`.
# 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`.
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)))
key = arg.args[1]
val = arg.args[2]
if val isa Symbol && val in kwarg_keys
# Case 1: chained override — resolve the reference in the elixir
push!(kwarg_exprs, Expr(:kw, key, QuoteNode(Expr(:block, val))))
elseif val isa Symbol
# Cases 2 & 3: use @isdefined to capture locally-defined values while
# falling back to an expression that resolves elixir-internal references
# in the elixir
push!(kwarg_exprs,
Expr(:kw, key,
esc(:((@isdefined $val) ? $val :
$(QuoteNode(Expr(:block, val)))))))
else
# 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
# Build the kwarg expressions that are forwarded to `trixi_include`. The rules for
# this are shared with packages defining their own testing macros, see
# `trixi_include_kwargs`.
local kwarg_exprs = trixi_include_kwargs(args;
reserved = (:additional_ignore_content,
:l2, :linf,
:RealT_for_test_tolerances,
:atol, :rtol))

# 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
Expand Down
Loading