Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 10 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ TrixiTest.jl follows the interpretation of
used in the Julia ecosystem. Notable changes will be documented in this file
for human readability.

## Changes in the v0.2 lifecycle

#### Added

- New function `trixi_include_kwargs` that turns the arguments of a
testing macro into a vector of `Expr(:kw, ...)` expressions that
can be spliced into a `trixi_include` call inside the expression
the macro returns ([#40]).

## Breaking changes from v0.1.x to v0.2

- The keyword argument `RealT` in the macro `@test_trixi_include_base`
has been renamed to `RealT_for_test_tolerances` to avoid name clashes
when passing a keyword argument `RealT`.
when passing a keyword argument `RealT` ([#20]).
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
79 changes: 79 additions & 0 deletions src/auxiliary.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,82 @@ 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` (from [TrixiBase.jl](https://github.com/trixi-framework/TrixiBase.jl))
call inside the expression the macro returns:

```julia
macro my_test_include(example, args...)
local kwargs = trixi_include_kwargs(args; reserved = (:l2, :linf))
quote
@trixi_test_nowarn trixi_include(@__MODULE__, \$(esc(example)); \$(kwargs...))
# Use the values passed to the `reserved` arguments `l2` and `linf`.
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