Skip to content

Commit 7570995

Browse files
committed
Show evaluated test arguments from broadcast functions
1 parent e985652 commit 7570995

File tree

2 files changed

+247
-144
lines changed

2 files changed

+247
-144
lines changed

stdlib/Test/src/Test.jl

Lines changed: 136 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -340,46 +340,55 @@ 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(comparisons::Tuple, quoted::Expr, source::LineNumberNode, negate::Bool=false)
345344
quoted_args = quoted.args
346-
n = length(evaled_args)
345+
n = length(comparisons)
347346
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
361347

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

385394
if negate
@@ -576,14 +585,90 @@ macro test_skip(ex, kws...)
576585
return :(record(get_testset(), $testres))
577586
end
578587

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

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

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

589674
# An internal function, called by the code generated by the @test
@@ -613,60 +698,22 @@ function get_test_result(ex, source)
613698
ex = Expr(:comparison, ex.args[1], ex.head, ex.args[2])
614699
end
615700
if isa(ex, Expr) && ex.head === :comparison
616-
# pass all terms of the comparison to `eval_comparison`, as an Expr
701+
# pass all terms of the comparison to `eval_test_comparison`, as a tuple
617702
escaped_terms = [esc(arg) for arg in ex.args]
618703
quoted_terms = [QuoteNode(arg) for arg in ex.args]
619-
testret = :(eval_test(
620-
Expr(:comparison, $(escaped_terms...)),
704+
testret = :(eval_test_comparison(
705+
($(escaped_terms...),),
621706
Expr(:comparison, $(quoted_terms...)),
622707
$(QuoteNode(source)),
623708
$negate,
624709
))
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),
710+
elseif _should_escape_call(ex)
711+
call = _escape_call(ex)
712+
testret = :(eval_test_function(
713+
$(call.func),
714+
($(call.args...),),
715+
($(call.kwargs...),),
716+
$(call.quoted_func),
670717
$(QuoteNode(source)),
671718
$negate,
672719
))

0 commit comments

Comments
 (0)