Skip to content

Commit bb62478

Browse files
committed
runtests: filter statically toplevel testsets-for
Unfortunately making the code quite more complex :( But testset-for is expensive to compile, so this is a nice to have change.
1 parent f8bc817 commit bb62478

File tree

2 files changed

+88
-21
lines changed

2 files changed

+88
-21
lines changed

src/InlineTest.jl

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,22 @@ using .Testset: Testset, @testsetr
2727
__init__() = INLINE_TEST[] = gensym()
2828

2929

30+
struct TestsetExpr
31+
desc::Union{String,Expr}
32+
loops::Union{Expr,Nothing}
33+
body::Expr
34+
final::Bool
35+
end
36+
3037
function tests(m)
3138
inline_test::Symbol = m (InlineTest, InlineTest.InlineTestTest) ? :__INLINE_TEST__ : INLINE_TEST[]
3239
if !isdefined(m, inline_test)
33-
@eval m $inline_test = Tuple{Expr,Union{String,Missing},Bool}[]
40+
@eval m $inline_test = []
3441
end
3542
getfield(m, inline_test)
3643
end
3744

38-
replacetestset(x) = x, false
45+
replacetestset(x) = (x, missing, false)
3946

4047
# replace unqualified `@testset` by @testsetr
4148
# return also (as 3nd element) whether the expression contains a (possibly nested) @testset
@@ -50,21 +57,41 @@ function replacetestset(x::Expr)
5057
:($(Testset.FINAL[]) = $final),
5158
Expr(:macrocall, Expr(:., :InlineTest, QuoteNode(Symbol("@testsetr"))),
5259
map(first, body)...)),
53-
final, true)
60+
final,
61+
true)
5462
else
5563
body = map(replacetestset, x.args)
5664
(Expr(x.head, map(first, body)...),
57-
missing, any(last, body)) # missing: a non-testset doesn't have a "final" attribute...
65+
missing, # missing: a non-testset doesn't have a "final" attribute...
66+
any(last, body))
5867
end
5968
end
6069

6170
function addtest(args::Tuple, m::Module)
62-
desc = args[1] isa String ? args[1] : missing
63-
# args[1] might not be a string if none was passed, or for a testset-for with
64-
# interpolated loop variable (in which case it's difficult to statically know the
65-
# final description)
66-
ts, final, _ = replacetestset(:(@testset($(args...))))
67-
push!(tests(m), (ts, desc, final))
71+
length(args) == 2 || error("unsupported @testset")
72+
73+
desc = args[1]
74+
desc isa String || Meta.isexpr(desc, :string) || error("unsupported @testset")
75+
76+
body = args[2]
77+
isa(body, Expr) || error("Expected begin/end block or for loop as argument to @testset")
78+
if body.head === :for
79+
isloop = true
80+
elseif body.head === :block
81+
isloop = false
82+
else
83+
error("Expected begin/end block or for loop as argument to @testset")
84+
end
85+
86+
if isloop
87+
loops = body.args[1]
88+
expr, _, has_testset = replacetestset(body.args[2])
89+
final = !has_testset
90+
push!(tests(m), TestsetExpr(desc, loops, expr, final))
91+
else
92+
ts, final, _ = replacetestset(:(@testset $desc $body))
93+
push!(tests(m), TestsetExpr(desc, nothing, ts, final))
94+
end
6895
nothing
6996
end
7097

@@ -95,27 +122,66 @@ in which it was written (e.g. `m`, when specified).
95122
"""
96123
function runtests(m::Module, regex::Regex = r""; wrap::Bool=false)
97124
partial = partialize(regex)
125+
matches(desc, final) = Testset.partialoccursin((partial, regex)[1+final], desc)
126+
98127
if wrap
99128
Core.eval(m, :(InlineTest.Test.@testset $("Tests for module $m") begin
100-
let $(Testset.REGEX[]) = ($partial, $regex)
101-
$(map(first, tests(m))...)
102-
end
129+
$(map(ts -> wrap_ts(partial, regex, ts), tests(m))...)
103130
end))
104131
else
105-
for (ts, desc, final) in tests(m)
106-
# bypass evaluation if we know statically that testset won't be run
107-
if desc isa String && !Testset.partialoccursin((partial, regex)[1+final], desc)
108-
continue
109-
end
132+
for ts in tests(m)
110133
# it's faster to evel in a loop than to eval a block containing tests(m)
111-
Core.eval(m, :(let $(Testset.REGEX[]) = ($partial, $regex)
112-
$ts
113-
end))
134+
desc = ts.desc
135+
if ts.loops === nothing # begin/end testset
136+
@assert desc isa String
137+
# bypass evaluation if we know statically that testset won't be run
138+
matches(desc, ts.final) ||
139+
continue
140+
Core.eval(m, wrap_ts(partial, regex, ts))
141+
else # for-loop testset
142+
loops = ts.loops
143+
xs = Core.eval(m, loops.args[2]) # loop values
144+
# we eval the description to a string for each values, and if at least one matches
145+
# the filtering-regex, then we must instantiate the testset (otherwise, it's skipped)
146+
skip = true
147+
for x in xs
148+
# TODO: do not assign loop.args[1] in m ?
149+
Core.eval(m, Expr(:(=), loops.args[1], x))
150+
descx = Core.eval(m, desc)
151+
if matches(descx, ts.final)
152+
skip = false
153+
break
154+
end
155+
end
156+
skip && continue
157+
Core.eval(m, wrap_ts(partial, regex, ts, xs))
158+
end
114159
end
115160
end
116161
nothing
117162
end
118163

164+
function wrap_ts(partial, regex, ts::TestsetExpr, loopvals=nothing)
165+
if ts.loops === nothing
166+
quote
167+
let $(Testset.REGEX[]) = ($partial, $regex)
168+
$(ts.body)
169+
end
170+
end
171+
else
172+
loopvals = something(loopvals, ts.loops.args[2])
173+
quote
174+
let $(Testset.REGEX[]) = ($partial, $regex),
175+
$(Testset.FINAL[]) = $(ts.final)
176+
177+
InlineTest.@testsetr $(ts.desc) for $(ts.loops.args[1]) in $loopvals
178+
$(ts.body)
179+
end
180+
end
181+
end
182+
end
183+
end
184+
119185
function runtests(; wrap::Bool=true)
120186
foreach(values(Base.loaded_modules)) do m
121187
if isdefined(m, INLINE_TEST[]) # will automatically skip InlineTest and InlineTest.InlineTestTest

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,4 @@ check(".*h1", ["c", "f1", "h1"])
7272
check(".*h\$", ["c", "f1"])
7373

7474
runtests(M, wrap=true) # TODO: more precise tests
75+
runtests(M, wrap=false)

0 commit comments

Comments
 (0)