Skip to content

Commit d7c0102

Browse files
committed
hijack: add include=:static option
Fix partially #29.
1 parent d9b6e5c commit d7c0102

6 files changed

+150
-40
lines changed

src/ReTest.jl

+42-8
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ using .Testset: Testset, Format, print_id
5757
Base.@kwdef mutable struct Options
5858
verbose::Bool = false # annotated verbosity
5959
transient_verbose::Bool = false # verbosity for next run
60+
static_include::Bool = false # whether to execute include at `replace_ts` time
6061
end
6162

6263
mutable struct TestsetExpr
@@ -108,13 +109,25 @@ struct _Invalid
108109
global const invalid = _Invalid.instance
109110
end
110111

112+
function extract_testsets(dest)
113+
function extractor(x)
114+
if Meta.isexpr(x, :macrocall) && x.args[1] == Symbol("@testset")
115+
push!(dest, x)
116+
nothing # @testset move out of the evaluated file
117+
else
118+
x
119+
end
120+
end
121+
end
122+
111123
# replace unqualified `@testset` by TestsetExpr
112-
function replace_ts(source, mod, x::Expr, parent)
124+
function replace_ts(source, mod, x::Expr, parent; static_include::Bool)
113125
if x.head === :macrocall
114126
name = x.args[1]
115127
if name === Symbol("@testset")
116128
@assert x.args[2] isa LineNumberNode
117-
ts, hasbroken = parse_ts(x.args[2], mod, Tuple(x.args[3:end]), parent)
129+
ts, hasbroken = parse_ts(x.args[2], mod, Tuple(x.args[3:end]), parent;
130+
static_include=static_include)
118131
ts !== invalid && parent !== nothing && push!(parent.children, ts)
119132
ts, false # hasbroken counts only "proper" @test_broken, not recursive ones
120133
elseif name === Symbol("@test_broken")
@@ -123,7 +136,7 @@ function replace_ts(source, mod, x::Expr, parent)
123136
# `@test` is generally called a lot, so it's probably worth it to skip
124137
# the containment test in this case
125138
x = macroexpand(mod, x, recursive=false)
126-
replace_ts(source, mod, x, parent)
139+
replace_ts(source, mod, x, parent; static_include=static_include)
127140
else
128141
@goto default
129142
end
@@ -133,18 +146,34 @@ function replace_ts(source, mod, x::Expr, parent)
133146
x.args[end] = path isa AbstractString ?
134147
joinpath(sourcepath, path) :
135148
:(joinpath($sourcepath, $path))
136-
x, false
149+
if static_include
150+
length(x.args) == 2 || error("cannot handle include with two arguments: $x")
151+
news = Expr(:block)
152+
insert!(x.args, 2, extract_testsets(news.args))
153+
try
154+
Core.eval(mod, x)
155+
catch
156+
@warn "could not statically include at $source"
157+
deleteat!(x.args, 2)
158+
return x, false
159+
end
160+
replace_ts(source, mod, news, parent; static_include=static_include)
161+
else
162+
x, false
163+
end
137164
else @label default
138-
body_br = map(z -> replace_ts(source, mod, z, parent), x.args)
165+
body_br = map(z -> replace_ts(source, mod, z, parent; static_include=static_include),
166+
x.args)
139167
filter!(x -> first(x) !== invalid, body_br)
140168
Expr(x.head, first.(body_br)...), any(last.(body_br))
141169
end
142170
end
143171

144-
replace_ts(source, mod, x, _) = x, false
172+
replace_ts(source, mod, x, _1; static_include::Bool) = x, false
145173

146174
# create a TestsetExpr from @testset's args
147-
function parse_ts(source::LineNumberNode, mod::Module, args::Tuple, parent=nothing)
175+
function parse_ts(source::LineNumberNode, mod::Module, args::Tuple, parent=nothing;
176+
static_include::Bool=false)
148177
function tserror(msg)
149178
@error msg _file=String(source.file) _line=source.line _module=mod
150179
invalid, false
@@ -158,6 +187,10 @@ function parse_ts(source::LineNumberNode, mod::Module, args::Tuple, parent=nothi
158187
marks = Marks()
159188
if parent !== nothing
160189
append!(marks.hard, parent.marks.hard) # copy! not available in Julia 1.0
190+
options.static_include = parent.options.static_include
191+
# if static_include was set in parent, it should have been forwarded also
192+
# through the parse_ts/replace_ts call chains:
193+
@assert static_include == parent.options.static_include
161194
end
162195
for arg in args[1:end-1]
163196
if arg isa String || Meta.isexpr(arg, :string)
@@ -207,7 +240,8 @@ function parse_ts(source::LineNumberNode, mod::Module, args::Tuple, parent=nothi
207240
end
208241

209242
ts = TestsetExpr(source, mod, desc, options, marks, loops, parent)
210-
ts.body, ts.hasbroken = replace_ts(source, mod, tsbody, ts)
243+
ts.body, ts.hasbroken = replace_ts(source, mod, tsbody, ts;
244+
static_include=options.static_include)
211245
ts, false # hasbroken counts only "proper" @test_broken, not recursive ones
212246
end
213247

src/hijack.jl

+68-30
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const loaded_testmodules = Dict{Module,Vector{Module}}()
104104

105105
"""
106106
ReTest.hijack(source, [modname];
107-
parentmodule::Module=Main, lazy=false, testset::Bool=false,
107+
parentmodule::Module=Main, lazy=false, [include::Symbol],
108108
[revise::Bool])
109109
110110
Given test files defined in `source` using the `Test` package, try to load
@@ -143,11 +143,10 @@ The `lazy` keyword specifies whether some toplevel expressions should be skipped
143143
* `:brutal` means toplevel `@test*` macros are removed, as well as toplevel
144144
`begin`, `let`, `for` or `if` blocks.
145145
146-
#### `testset` keyword
146+
#### `include` keyword
147147
148-
The `testset` keyword can help to handle the case where `@testset`s contain
149-
`include` expressions (at the "toplevel" of the testset), like in the
150-
following example:
148+
The `include` keyword can help to handle the case where `@testset`s contain
149+
`include` expressions, like in the following example:
151150
```julia
152151
@testset "parent" begin
153152
@test true
@@ -161,11 +160,28 @@ of the testset which include them. With `ReTest`, the `include` expressions
161160
would be evaluated only when the parent testsets are run, so that included
162161
testsets are not run themselves, but only "declared".
163162
164-
It the `testset` keyword
165-
is `true`, `hijack` inspects `@testset` expressions and puts `include`
166-
expressions outside of the testset. This is not ideal, but at least allows
167-
`ReTest` to know about all the testsets right after the call to `hijack`, and
168-
to not declare new testsets when parent testsets are run.
163+
If the `include` keyword is set to `:static`, `include(...)` expressions are
164+
evaluated when `@testset` expressions containing them are parsed, before
165+
filtering and before testsets are run. Testsets which are declared (within the
166+
same module) as a side effect of `include(...)` are then inserted in place of
167+
the call to `include(...)`.
168+
169+
If the `include` keyword is set to `:outline`, `hijack` inspects topelevel
170+
`@testset` expressions and puts toplevel `include(...)` expressions outside of
171+
the containing testset, and should therefore be evaluated immediately. This is
172+
not ideal, but at least allows `ReTest` to know about all the testsets right
173+
after the call to `hijack`, and to not declare new testsets when parent
174+
testsets are run.
175+
176+
The `:outline` option might be deprecated in the future, and `include=:static`
177+
should generally be preferred. One case where `:outline` might work better is
178+
when the included file defines a submodule: `ReTest` doesn't have the concept
179+
of a nested testset belonging to a different module than the parent testset,
180+
so the best that can be done here is to "outline" such nested testsets; with
181+
`include=:outline`, `hijack` will "process" the content of such submodules
182+
(replace `using Test` by `using ReTest`, etc.), whereas with
183+
`include=:static`, the subdmodules will get defined after `hijack` has
184+
returned (on the first call to `retest` thereafter), so won't be "processed".
169185
170186
#### `revise` keyword
171187
@@ -180,8 +196,12 @@ and to `false` otherwise.
180196
"""
181197
function hijack end
182198

199+
# TODO 0.4: remove `testset` kwarg
200+
# TODO: maybe deprecate `include=:outline`?
201+
183202
function hijack(path::AbstractString, modname=nothing; parentmodule::Module=Main,
184-
lazy=false, testset::Bool=false, revise::Maybe{Bool}=nothing)
203+
lazy=false, revise::Maybe{Bool}=nothing,
204+
include::Maybe{Symbol}=nothing, testset::Bool=false)
185205

186206
# do first, to error early if necessary
187207
Revise = get_revise(revise)
@@ -192,22 +212,34 @@ function hijack(path::AbstractString, modname=nothing; parentmodule::Module=Main
192212
modname = Symbol(modname)
193213

194214
newmod = @eval parentmodule module $modname end
195-
populate_mod!(newmod, path; lazy=lazy, testset=testset, Revise=Revise)
215+
populate_mod!(newmod, path; lazy=lazy, include=setinclude(include, testset),
216+
Revise=Revise)
196217
newmod
197218
end
198219

220+
function setinclude(include, testset)
221+
if testset
222+
include === nothing || error("cannot specify both `testset` and `include` arguments")
223+
:outline
224+
else
225+
include === nothing || include === :static || include === :outline ||
226+
error("`include` keyword only accepts `:static` or `:outline` as value")
227+
include
228+
end
229+
end
230+
199231
# this is just a work-around for v"1.5", where @__MODULE__ can't be used in
200232
# expressions; root_module[] is set equal to @__MODULE__ within modules
201233
const root_module = Ref{Symbol}()
202234

203235
__init__() = root_module[] = gensym("MODULE")
204236

205-
function populate_mod!(mod::Module, path; lazy, Revise, testset)
237+
function populate_mod!(mod::Module, path; lazy, Revise, include)
206238
lazy (true, false, :brutal) ||
207239
throw(ArgumentError("the `lazy` keyword must be `true`, `false` or `:brutal`"))
208240

209241
files = Revise === nothing ? nothing : Dict(path => mod)
210-
substitute!(x) = substitute_retest!(x, lazy, testset, files)
242+
substitute!(x) = substitute_retest!(x, lazy, include, files)
211243

212244
@eval mod begin
213245
using ReTest # for files which don't have `using Test`
@@ -235,7 +267,8 @@ function revise_track(Revise, files)
235267
end
236268

237269
function hijack(packagemod::Module, modname=nothing; parentmodule::Module=Main,
238-
lazy=false, testset::Bool=false, revise::Maybe{Bool}=nothing)
270+
lazy=false, revise::Maybe{Bool}=nothing,
271+
include::Maybe{Symbol}=nothing, testset::Bool=false)
239272
packagepath = pathof(packagemod)
240273
packagepath === nothing && packagemod !== Base &&
241274
throw(ArgumentError("$packagemod is not a package"))
@@ -253,13 +286,13 @@ function hijack(packagemod::Module, modname=nothing; parentmodule::Module=Main,
253286
else
254287
path = joinpath(dirname(dirname(packagepath)), "test", "runtests.jl")
255288
hijack(path, modname, parentmodule=parentmodule,
256-
lazy=lazy, testset=testset, revise=revise)
289+
lazy=lazy, testset=testset, include=include, revise=revise)
257290
end
258291
end
259292

260-
function substitute_retest!(ex, lazy, testset, files=nothing;
293+
function substitute_retest!(ex, lazy, include_, files=nothing;
261294
ishijack::Bool=true)
262-
substitute!(x) = substitute_retest!(x, lazy, testset, files, ishijack=ishijack)
295+
substitute!(x) = substitute_retest!(x, lazy, include_, files, ishijack=ishijack)
263296

264297
if Meta.isexpr(ex, :using)
265298
ishijack || return ex
@@ -309,19 +342,24 @@ function substitute_retest!(ex, lazy, testset, files=nothing;
309342
ishijack || return ex
310343
if lazy != false && ex.args[1] TEST_MACROS
311344
empty_expr!(ex)
312-
elseif testset && ex.args[1] == Symbol("@testset")
313-
# we remove `include` expressions and put them out of the `@testset`
314-
body = ex.args[end]
315-
if body.head == :for
316-
body = body.args[end]
345+
elseif include_ !== nothing && ex.args[1] == Symbol("@testset")
346+
if include_ === :outline
347+
# we remove `include` expressions and put them out of the `@testset`
348+
body = ex.args[end]
349+
if body.head == :for
350+
body = body.args[end]
351+
end
352+
includes = splice!(body.args, findall(body.args) do x
353+
Meta.isexpr(x, :call) && x.args[1] == :include
354+
end)
355+
map!(substitute!, includes, includes)
356+
ex.head = :block
357+
newts = Expr(:macrocall, ex.args...)
358+
push!(empty!(ex.args), newts, includes...)
359+
else # :static
360+
pos = ex.args[2] isa LineNumberNode ? 3 : 2
361+
insert!(ex.args, pos, :(static_include=true))
317362
end
318-
includes = splice!(body.args, findall(body.args) do x
319-
Meta.isexpr(x, :call) && x.args[1] == :include
320-
end)
321-
map!(substitute!, includes, includes)
322-
ex.head = :block
323-
newts = Expr(:macrocall, ex.args...)
324-
push!(empty!(ex.args), newts, includes...)
325363
end
326364
elseif ex isa Expr && ex.head (:block, :let, :for, :while, :if, :try)
327365
if lazy == :brutal

test/Hijack/test/include_static.jl

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# include = :static
2+
using Hijack, Test
3+
4+
@testset "include_static" begin
5+
@test true
6+
push!(Hijack.RUN, 1)
7+
include("include_static_included1.jl")
8+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@testset "include_static_included1 $i" for i=1:2
2+
@test true
3+
@testset "nested include_static_included1" begin
4+
@test true
5+
push!(Hijack.RUN, 2)
6+
# test that `include` kwarg is forwarded
7+
include("include_static_included2.jl")
8+
end
9+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@testset "include_static_included2" begin
2+
@test true
3+
push!(Hijack.RUN, 3)
4+
end

test/runtests.jl

+19-2
Original file line numberDiff line numberDiff line change
@@ -1790,10 +1790,27 @@ end
17901790
empty!(Hijack.RUN)
17911791
@test_throws ArgumentError ReTest.hijack("./Hijack/test/lazy.jl", :HijackWrong, lazy=:wrong)
17921792

1793-
# test testset=true
1793+
# test include=:outline
17941794
empty!(Hijack.RUN)
1795-
ReTest.hijack("./Hijack/test/testset.jl", :HijackTestset, testset=true)
1795+
ReTest.hijack("./Hijack/test/testset.jl", :HijackTestset, include=:outline)
17961796
retest(HijackTestset)
17971797
@test Hijack.RUN == [1, 2, 3]
1798+
1799+
# test include=:static
1800+
empty!(Hijack.RUN)
1801+
@test_throws ErrorException ReTest.hijack("./Hijack/test/include_static.jl", :HijackInclude, include=:static, testset=true)
1802+
@test_throws ErrorException ReTest.hijack("./Hijack/test/include_static.jl", :HijackInclude, include=:notvalid)
1803+
ReTest.hijack("./Hijack/test/include_static.jl", :HijackInclude, include=:static)
1804+
check(HijackInclude, dry=true, verbose=9, [], output="""
1805+
1| include_static
1806+
2| include_static_included1 1
1807+
3| nested include_static_included1
1808+
4| include_static_included2
1809+
2| include_static_included1 2
1810+
3| nested include_static_included1
1811+
4| include_static_included2
1812+
""")
1813+
retest(HijackInclude)
1814+
@test Hijack.RUN == [1, 2, 3, 2, 3]
17981815
end
17991816
end

0 commit comments

Comments
 (0)