Skip to content

Commit f32789b

Browse files
authored
hijack: allow specifying custom include-like functions (#34)
1 parent 697e528 commit f32789b

File tree

4 files changed

+84
-31
lines changed

4 files changed

+84
-31
lines changed

src/ReTest.jl

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Base.@kwdef mutable struct Options
5858
verbose::Bool = false # annotated verbosity
5959
transient_verbose::Bool = false # verbosity for next run
6060
static_include::Bool = false # whether to execute include at `replace_ts` time
61+
include_functions::Vector{Symbol} = [:include] # functions to treat like include
6162
end
6263

6364
mutable struct TestsetExpr
@@ -121,12 +122,14 @@ function extract_testsets(dest)
121122
end
122123

123124
# replace unqualified `@testset` by TestsetExpr
124-
function replace_ts(source, mod, x::Expr, parent; static_include::Bool)
125+
function replace_ts(source, mod, x::Expr, parent; static_include::Bool,
126+
include_functions::Vector{Symbol})
125127
if x.head === :macrocall
126128
name = x.args[1]
127129
if name === Symbol("@testset")
128130
@assert x.args[2] isa LineNumberNode
129131
ts, hasbroken = parse_ts(x.args[2], mod, Tuple(x.args[3:end]), parent;
132+
include_functions=include_functions,
130133
static_include=static_include)
131134
ts !== invalid && parent !== nothing && push!(parent.children, ts)
132135
ts, false # hasbroken counts only "proper" @test_broken, not recursive ones
@@ -136,16 +139,17 @@ function replace_ts(source, mod, x::Expr, parent; static_include::Bool)
136139
# `@test` is generally called a lot, so it's probably worth it to skip
137140
# the containment test in this case
138141
x = macroexpand(mod, x, recursive=false)
139-
replace_ts(source, mod, x, parent; static_include=static_include)
142+
replace_ts(source, mod, x, parent; static_include=static_include,
143+
include_functions=include_functions)
140144
else
141145
@goto default
142146
end
143-
elseif x.head == :call && x.args[1] == :include
147+
elseif x.head == :call && x.args[1] include_functions
144148
path = x.args[end]
145149
sourcepath = dirname(string(source.file))
146150
x.args[end] = path isa AbstractString ?
147-
joinpath(sourcepath, path) :
148-
:(joinpath($sourcepath, $path))
151+
joinpath(sourcepath, path) :
152+
:(joinpath($sourcepath, $path))
149153
if static_include
150154
news = InlineTest.get_tests(mod).news
151155
newslen = length(news)
@@ -166,26 +170,30 @@ function replace_ts(source, mod, x::Expr, parent; static_include::Bool)
166170
# below; it's currently not very important
167171
tsi.source, tsi.ts...)
168172
for tsi in newstmp)...)
169-
replace_ts(source, mod, included_ts, parent; static_include=static_include)
173+
replace_ts(source, mod, included_ts, parent;
174+
static_include=static_include, include_functions=include_functions)
170175
else
171176
nothing, false
172177
end
173178
else
174179
x, false
175180
end
176181
else @label default
177-
body_br = map(z -> replace_ts(source, mod, z, parent; static_include=static_include),
182+
body_br = map(z -> replace_ts(source, mod, z, parent; static_include=static_include,
183+
include_functions=include_functions),
178184
x.args)
179185
filter!(x -> first(x) !== invalid, body_br)
180186
Expr(x.head, first.(body_br)...), any(last.(body_br))
181187
end
182188
end
183189

184-
replace_ts(source, mod, x, _1; static_include::Bool) = x, false
190+
replace_ts(source, mod, x, _1; static_include::Bool,
191+
include_functions) = x, false
185192

186193
# create a TestsetExpr from @testset's args
187194
function parse_ts(source::LineNumberNode, mod::Module, args::Tuple, parent=nothing;
188-
static_include::Bool=false)
195+
static_include::Bool=false, include_functions::Vector{Symbol}=[:include])
196+
189197
function tserror(msg)
190198
@error msg _file=String(source.file) _line=source.line _module=mod
191199
invalid, false
@@ -195,14 +203,15 @@ function parse_ts(source::LineNumberNode, mod::Module, args::Tuple, parent=nothi
195203
return tserror("expected begin/end block or for loop as argument to @testset")
196204

197205
local desc
198-
options = Options()
206+
options = Options(include_functions=include_functions)
199207
marks = Marks()
200208
if parent !== nothing
201209
append!(marks.hard, parent.marks.hard) # copy! not available in Julia 1.0
202210
options.static_include = parent.options.static_include
203211
# if static_include was set in parent, it should have been forwarded also
204212
# through the parse_ts/replace_ts call chains:
205213
@assert static_include == parent.options.static_include
214+
@assert include_functions === parent.options.include_functions
206215
end
207216
for arg in args[1:end-1]
208217
if arg isa String || Meta.isexpr(arg, :string)
@@ -211,10 +220,22 @@ function parse_ts(source::LineNumberNode, mod::Module, args::Tuple, parent=nothi
211220
# TODO: support non-literal symbols?
212221
push!(marks.hard, arg.value)
213222
elseif Meta.isexpr(arg, :(=))
214-
arg.args[1] in fieldnames(Options) ||
215-
return tserror("unsupported @testset option")
216-
# TODO: make that work with non-literals:
217-
setfield!(options, arg.args[1], arg.args[2])
223+
optname = arg.args[1]
224+
optname in fieldnames(Options) ||
225+
return tserror("unsupported @testset option: $optname")
226+
if optname == :include_functions
227+
@assert Meta.isexpr(arg.args[2], :vect)
228+
if parent !== nothing
229+
options.include_functions = Symbol[] # make it non-shared
230+
end
231+
for ifn in arg.args[2].args
232+
@assert ifn isa QuoteNode
233+
push!(options.include_functions, ifn.value)
234+
end
235+
else
236+
# TODO: make that work with non-literals:
237+
setfield!(options, optname, arg.args[2])
238+
end
218239
else
219240
return tserror("unsupported @testset")
220241
end
@@ -253,7 +274,8 @@ function parse_ts(source::LineNumberNode, mod::Module, args::Tuple, parent=nothi
253274

254275
ts = TestsetExpr(source, mod, desc, options, marks, loops, parent)
255276
ts.body, ts.hasbroken = replace_ts(source, mod, tsbody, ts;
256-
static_include=options.static_include)
277+
static_include=options.static_include,
278+
include_functions=options.include_functions)
257279
ts, false # hasbroken counts only "proper" @test_broken, not recursive ones
258280
end
259281

src/hijack.jl

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ const loaded_testmodules = Dict{Module,Vector{Module}}()
104104

105105
"""
106106
ReTest.hijack(source, [modname];
107-
parentmodule::Module=Main, lazy=false, [include::Symbol],
108-
[revise::Bool])
107+
parentmodule::Module=Main, lazy=false, [revise::Bool],
108+
[include::Symbol], include_functions=[:include])
109109
110110
Given test files defined in `source` using the `Test` package, try to load
111111
them by replacing `Test` with `ReTest`, wrapping them in a module `modname`
@@ -183,6 +183,15 @@ so the best that can be done here is to "outline" such nested testsets; with
183183
`include=:static`, the subdmodules will get defined after `hijack` has
184184
returned (on the first call to `retest` thereafter), so won't be "processed".
185185
186+
#### `include_functions` keyword
187+
188+
When the `include=:static` keyword argument is passed, it's possible to
189+
tell `hijack` to apply the same treatment to other functions than
190+
`include`, by passing a list a symbols to `include_functions`.
191+
For example, if you defined a custom function `custom_include(x)`
192+
which itself calls out to `include`, you can pass
193+
`include_functions=[:custom_include]` to `hijack`.
194+
186195
#### `revise` keyword
187196
188197
The `revise` keyword specifies whether `Revise` should be used to track
@@ -201,7 +210,8 @@ function hijack end
201210

202211
function hijack(path::AbstractString, modname=nothing; parentmodule::Module=Main,
203212
lazy=false, revise::Maybe{Bool}=nothing,
204-
include::Maybe{Symbol}=nothing, testset::Bool=false)
213+
include::Maybe{Symbol}=nothing, testset::Bool=false,
214+
include_functions=[:include])
205215

206216
# do first, to error early if necessary
207217
Revise = get_revise(revise)
@@ -213,6 +223,7 @@ function hijack(path::AbstractString, modname=nothing; parentmodule::Module=Main
213223

214224
newmod = @eval parentmodule module $modname end
215225
populate_mod!(newmod, path; lazy=lazy, include=setinclude(include, testset),
226+
include_functions=include_functions,
216227
Revise=Revise)
217228
newmod
218229
end
@@ -234,12 +245,14 @@ const root_module = Ref{Symbol}()
234245

235246
__init__() = root_module[] = gensym("MODULE")
236247

237-
function populate_mod!(mod::Module, path; lazy, Revise, include::Maybe{Symbol}=nothing)
248+
function populate_mod!(mod::Module, path; lazy, Revise, include::Maybe{Symbol}=nothing,
249+
include_functions)
238250
lazy (true, false, :brutal) ||
239251
throw(ArgumentError("the `lazy` keyword must be `true`, `false` or `:brutal`"))
240252

241253
files = Revise === nothing ? nothing : Dict(path => mod)
242-
substitute!(x) = substitute_retest!(x, lazy, include, files)
254+
substitute!(x) = substitute_retest!(x, lazy, include, files;
255+
include_functions=include_functions)
243256

244257
@eval mod begin
245258
using ReTest # for files which don't have `using Test`
@@ -268,7 +281,8 @@ end
268281

269282
function hijack(packagemod::Module, modname=nothing; parentmodule::Module=Main,
270283
lazy=false, revise::Maybe{Bool}=nothing,
271-
include::Maybe{Symbol}=nothing, testset::Bool=false)
284+
include::Maybe{Symbol}=nothing, testset::Bool=false,
285+
include_functions=[:include])
272286
packagepath = pathof(packagemod)
273287
packagepath === nothing && packagemod !== Base &&
274288
throw(ArgumentError("$packagemod is not a package"))
@@ -286,13 +300,15 @@ function hijack(packagemod::Module, modname=nothing; parentmodule::Module=Main,
286300
else
287301
path = joinpath(dirname(dirname(packagepath)), "test", "runtests.jl")
288302
hijack(path, modname, parentmodule=parentmodule,
289-
lazy=lazy, testset=testset, include=include, revise=revise)
303+
lazy=lazy, testset=testset, include=include, revise=revise,
304+
include_functions=include_functions)
290305
end
291306
end
292307

293308
function substitute_retest!(ex, lazy, include_::Maybe{Symbol}, files=nothing;
294-
ishijack::Bool=true)
295-
substitute!(x) = substitute_retest!(x, lazy, include_, files, ishijack=ishijack)
309+
ishijack::Bool=true, include_functions::Vector{Symbol}=[:include])
310+
substitute!(x) = substitute_retest!(x, lazy, include_, files, ishijack=ishijack,
311+
include_functions=include_functions)
296312

297313
if Meta.isexpr(ex, :using)
298314
ishijack || return ex
@@ -349,16 +365,28 @@ function substitute_retest!(ex, lazy, include_::Maybe{Symbol}, files=nothing;
349365
if body.head == :for
350366
body = body.args[end]
351367
end
352-
includes = splice!(body.args, findall(body.args) do x
353-
Meta.isexpr(x, :call) && x.args[1] == :include
354-
end)
368+
includes = splice!(body.args,
369+
findall(body.args) do x
370+
Meta.isexpr(x, :call) && x.args[1] include_functions
371+
end)
355372
map!(substitute!, includes, includes)
356373
ex.head = :block
357374
newts = Expr(:macrocall, ex.args...)
358375
push!(empty!(ex.args), newts, includes...)
359-
else # :static
376+
elseif include_ == :static
360377
pos = ex.args[2] isa LineNumberNode ? 3 : 2
361378
insert!(ex.args, pos, :(static_include=true))
379+
if !isempty(include_functions)
380+
# enabled only for :static currently, would need to change ex.args
381+
# to newts.args in order to make it work for :outline
382+
ifn_expr = Expr(:vect)
383+
for ifn in include_functions
384+
push!(ifn_expr.args, QuoteNode(ifn))
385+
end
386+
insert!(ex.args, length(ex.args)-1, :(include_functions=$ifn_expr))
387+
end
388+
else
389+
@assert false
362390
end
363391
end
364392
elseif ex isa Expr && ex.head (:block, :let, :for, :while, :if, :try)

test/Hijack/test/include_static.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# include = :static
22
using Hijack, Test
33

4+
custom_include_function(f) = include(f)
5+
46
@testset "include_static" begin
57
@test true
68
push!(Hijack.RUN, 1)
7-
include("include_static_included1.jl")
9+
custom_include_function("include_static_included1.jl")
810
end

test/runtests.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,7 @@ end
961961
@chapter TestsetErrors begin
962962
@test_logs (
963963
:error, "expected begin/end block or for loop as argument to @testset") (
964-
:error, "unsupported @testset option") (
964+
:error, "unsupported @testset option: notexistingoption") (
965965
:error, "unsupported @testset" ) (
966966
:error, "expected begin/end block or for loop as argument to @testset") (
967967
:error, "expected begin/end block or for loop as argument to @testset"
@@ -1862,7 +1862,8 @@ end
18621862
empty!(Hijack.RUN)
18631863
@test_throws ErrorException ReTest.hijack("./Hijack/test/include_static.jl", :HijackInclude, include=:static, testset=true)
18641864
@test_throws ErrorException ReTest.hijack("./Hijack/test/include_static.jl", :HijackInclude, include=:notvalid)
1865-
ReTest.hijack("./Hijack/test/include_static.jl", :HijackInclude, include=:static)
1865+
ReTest.hijack("./Hijack/test/include_static.jl", :HijackInclude, include=:static,
1866+
include_functions=[:include, :custom_include_function])
18661867
check(HijackInclude, dry=true, verbose=9, [], output="""
18671868
1| include_static
18681869
2| include_static_included1 1

0 commit comments

Comments
 (0)