Skip to content

Commit d934b03

Browse files
authored
Show evaluated test arguments from broadcast functions (#57839)
While working on #57825 I noticed that broadcasted functions were not well supported by the `@test` macro and never showed the evaluated test arguments. I've updated the stdlib to support this which required some large enough refactoring that it seemed best to make a separate PR for this change. The changes include: - Avoid embedding function references into `Expr`. This made the code harder to reason aboue than it needed to be. - Breaking up `eval_test` into `eval_test_comparison` and `eval_test_function` due to changes to the functions arguments - Handle broadcast syntax for binary operators (e.g. `1 .== 1`) and function calls (e.g. `(==).(1, 1)`) - Create new `_escaped_call` function which abstracts escaping all of the arguments for a function call. Doing this made it handling the special call syntax for broadcasting easier. - Perform a single pass on function arguments when escaping where previously we did two passes. Depends on: - #57825
1 parent 79b39a5 commit d934b03

File tree

3 files changed

+257
-143
lines changed

3 files changed

+257
-143
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ Standard library changes
4040

4141
#### Test
4242

43+
* Test failures when using the `@test` macro now show evaluated arguments for all function calls ([#57825], [#57839]).
44+
4345
#### InteractiveUtils
4446

4547
External dependencies

stdlib/Test/src/Test.jl

Lines changed: 137 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -340,46 +340,57 @@ struct Threw <: ExecutionResult
340340
source::LineNumberNode
341341
end
342342

343-
function eval_test(evaluated::Expr, quoted::Expr, source::LineNumberNode, negate::Bool=false)
344-
evaled_args = evaluated.args
343+
function eval_test_comparison(comparison::Expr, quoted::Expr, source::LineNumberNode, negate::Bool=false)
344+
comparison.head === :comparison || throw(ArgumentError("$comparison is not a comparison expression"))
345+
comparison_args = comparison.args
345346
quoted_args = quoted.args
346-
n = length(evaled_args)
347+
n = length(comparison_args)
347348
kw_suffix = ""
348-
if evaluated.head === :comparison
349-
args = evaled_args
350-
res = true
351-
i = 1
352-
while i < n
353-
a, op, b = args[i], args[i+1], args[i+2]
354-
if res
355-
res = op(a, b)
356-
end
357-
quoted_args[i] = a
358-
quoted_args[i+2] = b
359-
i += 2
360-
end
361349

362-
elseif evaluated.head === :call
363-
op = evaled_args[1]
364-
kwargs = (evaled_args[2]::Expr).args # Keyword arguments from `Expr(:parameters, ...)`
365-
args = evaled_args[3:n]
366-
367-
res = op(args...; kwargs...)
368-
369-
# Create "Evaluated" expression which looks like the original call but has all of
370-
# the arguments evaluated
371-
func_sym = quoted_args[1]::Union{Symbol,Expr}
372-
if isempty(kwargs)
373-
quoted = Expr(:call, func_sym, args...)
374-
elseif func_sym === : && !res
375-
quoted = Expr(:call, func_sym, args...)
376-
kw_suffix = " ($(join(["$k=$v" for (k, v) in kwargs], ", ")))"
377-
else
378-
kwargs_expr = Expr(:parameters, [Expr(:kw, k, v) for (k, v) in kwargs]...)
379-
quoted = Expr(:call, func_sym, kwargs_expr, args...)
350+
res = true
351+
i = 1
352+
while i < n
353+
a, op, b = comparison_args[i], comparison_args[i+1], comparison_args[i+2]
354+
if res
355+
res = op(a, b)
380356
end
357+
quoted_args[i] = a
358+
quoted_args[i+2] = b
359+
i += 2
360+
end
361+
362+
if negate
363+
res = !res
364+
quoted = Expr(:call, :!, quoted)
365+
end
366+
367+
Returned(res,
368+
# stringify arguments in case of failure, for easy remote printing
369+
res === true ? quoted : sprint(print, quoted, context=(:limit => true)) * kw_suffix,
370+
source)
371+
end
372+
373+
function eval_test_function(func, args, kwargs, quoted_func::Union{Expr,Symbol}, source::LineNumberNode, negate::Bool=false)
374+
res = func(args...; kwargs...)
375+
376+
# Create "Evaluated" expression which looks like the original call but has all of
377+
# the arguments evaluated
378+
kw_suffix = ""
379+
if quoted_func === : && !res
380+
kw_suffix = " ($(join(["$k=$v" for (k, v) in kwargs], ", ")))"
381+
quoted_args = args
382+
elseif isempty(kwargs)
383+
quoted_args = args
381384
else
382-
throw(ArgumentError("Unhandled expression type: $(evaluated.head)"))
385+
kwargs_expr = Expr(:parameters, [Expr(:kw, k, v) for (k, v) in kwargs]...)
386+
quoted_args = [kwargs_expr, args...]
387+
end
388+
389+
# Properly render broadcast function call syntax, e.g. `(==).(1, 2)` or `Base.:(==).(1, 2)`.
390+
quoted = if isa(quoted_func, Expr) && quoted_func.head === :. && length(quoted_func.args) == 1
391+
Expr(:., quoted_func.args[1], Expr(:tuple, quoted_args...))
392+
else
393+
Expr(:call, quoted_func, quoted_args...)
383394
end
384395

385396
if negate
@@ -576,14 +587,90 @@ macro test_skip(ex, kws...)
576587
return :(record(get_testset(), $testres))
577588
end
578589

579-
function _can_escape_call(@nospecialize ex)
580-
ex.head === :call || return false
590+
function _should_escape_call(@nospecialize ex)
591+
isa(ex, Expr) || return false
592+
593+
args = if ex.head === :call
594+
ex.args[2:end]
595+
elseif ex.head === :. && length(ex.args) == 2 && isa(ex.args[2], Expr) && ex.args[2].head === :tuple
596+
# Support for broadcasted function calls (e.g. `(==).(1, 2)`)
597+
ex.args[2].args
598+
else
599+
# Expression is not a function call
600+
return false
601+
end
602+
603+
# Avoid further processing on calls without any arguments
604+
return length(args) > 0
605+
end
606+
607+
# Escapes all of the positional arguments and keywords of a function such that we can call
608+
# the function at runtime.
609+
function _escape_call(@nospecialize ex)
610+
if isa(ex, Expr) && ex.head === :call
611+
# Update broadcast comparison calls to the function call syntax
612+
# (e.g. `1 .== 1` becomes `(==).(1, 1)`)
613+
func_str = string(ex.args[1])
614+
escaped_func = if first(func_str) == '.'
615+
esc(Expr(:., Symbol(func_str[2:end])))
616+
else
617+
esc(ex.args[1])
618+
end
619+
quoted_func = QuoteNode(ex.args[1])
620+
args = ex.args[2:end]
621+
elseif isa(ex, Expr) && ex.head === :. && length(ex.args) == 2 && isa(ex.args[2], Expr) && ex.args[2].head === :tuple
622+
# Support for broadcasted function calls (e.g. `(==).(1, 2)`)
623+
escaped_func = if isa(ex.args[1], Expr) && ex.args[1].head == :.
624+
Expr(:call, Expr(:., :Broadcast, QuoteNode(:BroadcastFunction)), esc(ex.args[1]))
625+
else
626+
Expr(:., esc(ex.args[1]))
627+
end
628+
quoted_func = QuoteNode(Expr(:., ex.args[1]))
629+
args = ex.args[2].args
630+
else
631+
throw(ArgumentError("$ex is not a call expression"))
632+
end
633+
634+
escaped_args = []
635+
escaped_kwargs = []
581636

582-
# Broadcasted functions are not currently supported
583-
first(string(ex.args[1])) != '.' || return false
637+
# Positional arguments and keywords that occur before `;`. Note that the keywords are
638+
# being revised into a form we can splat.
639+
for a in args
640+
if isa(a, Expr) && a.head === :parameters
641+
continue
642+
elseif isa(a, Expr) && a.head === :kw
643+
# Keywords that occur before `;`. Note that the keywords are being revised into
644+
# a form we can splat.
645+
push!(escaped_kwargs, Expr(:call, :(=>), QuoteNode(a.args[1]), esc(a.args[2])))
646+
elseif isa(a, Expr) && a.head === :...
647+
push!(escaped_args, Expr(:..., esc(a.args[1])))
648+
else
649+
push!(escaped_args, esc(a))
650+
end
651+
end
584652

585-
# At least one positional argument or keyword
586-
return length(ex.args) > 1
653+
# Keywords that occur after ';'
654+
if length(args) > 0 && isa(args[1], Expr) && args[1].head === :parameters
655+
for kw in args[1].args
656+
if isa(kw, Expr) && kw.head === :kw
657+
push!(escaped_kwargs, Expr(:call, :(=>), QuoteNode(kw.args[1]), esc(kw.args[2])))
658+
elseif isa(kw, Expr) && kw.head === :...
659+
push!(escaped_kwargs, Expr(:..., esc(kw.args[1])))
660+
elseif isa(kw, Expr) && kw.head === :.
661+
push!(escaped_kwargs, Expr(:call, :(=>), QuoteNode(kw.args[2].value), esc(Expr(:., kw.args[1], QuoteNode(kw.args[2].value)))))
662+
elseif isa(kw, Symbol)
663+
push!(escaped_kwargs, Expr(:call, :(=>), QuoteNode(kw), esc(kw)))
664+
end
665+
end
666+
end
667+
668+
return (;
669+
func=escaped_func,
670+
args=escaped_args,
671+
kwargs=escaped_kwargs,
672+
quoted_func,
673+
)
587674
end
588675

589676
# An internal function, called by the code generated by the @test
@@ -613,60 +700,22 @@ function get_test_result(ex, source)
613700
ex = Expr(:comparison, ex.args[1], ex.head, ex.args[2])
614701
end
615702
if isa(ex, Expr) && ex.head === :comparison
616-
# pass all terms of the comparison to `eval_comparison`, as an Expr
703+
# pass all terms of the comparison to `eval_test_comparison`, as a tuple
617704
escaped_terms = [esc(arg) for arg in ex.args]
618705
quoted_terms = [QuoteNode(arg) for arg in ex.args]
619-
testret = :(eval_test(
706+
testret = :(eval_test_comparison(
620707
Expr(:comparison, $(escaped_terms...)),
621708
Expr(:comparison, $(quoted_terms...)),
622709
$(QuoteNode(source)),
623710
$negate,
624711
))
625-
elseif isa(ex, Expr) && _can_escape_call(ex)
626-
escaped_func = esc(ex.args[1])
627-
quoted_func = QuoteNode(ex.args[1])
628-
629-
escaped_args = []
630-
escaped_kwargs = []
631-
632-
# Keywords that occur before `;`. Note that the keywords are being revised into
633-
# a form we can splat.
634-
for a in ex.args[2:end]
635-
if isa(a, Expr) && a.head === :kw
636-
push!(escaped_kwargs, Expr(:call, :(=>), QuoteNode(a.args[1]), esc(a.args[2])))
637-
end
638-
end
639-
640-
# Keywords that occur after ';'
641-
parameters_expr = ex.args[2]
642-
if isa(parameters_expr, Expr) && parameters_expr.head === :parameters
643-
for a in parameters_expr.args
644-
if isa(a, Expr) && a.head === :kw
645-
push!(escaped_kwargs, Expr(:call, :(=>), QuoteNode(a.args[1]), esc(a.args[2])))
646-
elseif isa(a, Expr) && a.head === :...
647-
push!(escaped_kwargs, Expr(:..., esc(a.args[1])))
648-
elseif isa(a, Expr) && a.head === :.
649-
push!(escaped_kwargs, Expr(:call, :(=>), QuoteNode(a.args[2].value), esc(Expr(:., a.args[1], QuoteNode(a.args[2].value)))))
650-
elseif isa(a, Symbol)
651-
push!(escaped_kwargs, Expr(:call, :(=>), QuoteNode(a), esc(a)))
652-
end
653-
end
654-
end
655-
656-
# Positional arguments
657-
for a in ex.args[2:end]
658-
isa(a, Expr) && a.head in (:kw, :parameters) && continue
659-
660-
if isa(a, Expr) && a.head === :...
661-
push!(escaped_args, Expr(:..., esc(a.args[1])))
662-
else
663-
push!(escaped_args, esc(a))
664-
end
665-
end
666-
667-
testret = :(eval_test(
668-
Expr(:call, $escaped_func, Expr(:parameters, $(escaped_kwargs...)), $(escaped_args...)),
669-
Expr(:call, $quoted_func),
712+
elseif _should_escape_call(ex)
713+
call = _escape_call(ex)
714+
testret = :(eval_test_function(
715+
$(call.func),
716+
($(call.args...),),
717+
($(call.kwargs...),),
718+
$(call.quoted_func),
670719
$(QuoteNode(source)),
671720
$negate,
672721
))

0 commit comments

Comments
 (0)