Skip to content

Commit 18c8fc6

Browse files
committed
Add @test_noalloc convenience
For use in unit-testing, @test_noalloc is implemented as a way to check that a given call is not allocating. To avoid extra dependencies, this is implemented as a package extension (loaded with Test). Since package extensions can not themselves export bindings, the macro is implemented in AllocCheck.jl, but emits an informative error when called without the extension loaded.
1 parent 48a8c34 commit 18c8fc6

4 files changed

Lines changed: 82 additions & 1 deletion

File tree

Project.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ GPUCompiler = "61eb1bfa-7361-4325-ad38-22787b887f55"
99
LLVM = "929cbde3-209d-540e-8aea-75f648917ca0"
1010
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
1111

12+
[weakdeps]
13+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
14+
15+
[extensions]
16+
TestAlloc = "Test"
17+
1218
[compat]
1319
GPUCompiler = "0.24, 0.25"
1420
LLVM = "6.3"

ext/TestAlloc.jl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
module TestAlloc
2+
3+
using AllocCheck
4+
using Test
5+
6+
function AllocCheck._test_noalloc(__module__, __source__, expr, kws...)
7+
# Collect the broken/skip/ignore_throw keywords and remove them from the rest of keywords
8+
broken = [kw.args[2] for kw in kws if kw.args[1] === :broken]
9+
skip = [kw.args[2] for kw in kws if kw.args[1] === :skip]
10+
ignore_throw = [kw.args[2] for kw in kws if kw.args[1] === :ignore_throw]
11+
kws = filter(kw -> kw.args[1] (:skip, :broken, :ignore_throw), kws)
12+
# Validation of broken/skip keywords
13+
for (kw, name) in ((broken, :broken), (skip, :skip), (ignore_throw, :ignore_throw))
14+
if length(kw) > 1
15+
error("invalid test_noalloc macro call: cannot set $(name) keyword multiple times")
16+
end
17+
end
18+
if length(skip) > 0 && length(broken) > 0
19+
error("invalid test_noalloc macro call: cannot set both skip and broken keywords")
20+
end
21+
if !Meta.isexpr(expr, :call) || isempty(expr.args)
22+
error("invalid test_noalloc macro call: must be applied to a function call")
23+
end
24+
ex = Expr(:inert, Expr(:macrocall, Symbol("@test_noalloc"), nothing, expr))
25+
quote
26+
if $(length(skip) > 0 && esc(skip[1]))
27+
$(Test.record)($(Test.get_testset)(), $(Test.Broken)(:skipped, $ex))
28+
else
29+
result = try
30+
x = $(AllocCheck._check_allocs_call(
31+
expr, __module__, __source__; ignore_throw = length(ignore_throw) > 0 && ignore_throw[1]))
32+
Base.donotdelete(x)
33+
$(Test.Returned)(true, nothing, $(QuoteNode(__source__)))
34+
catch err
35+
if err isa InterruptException
36+
rethrow()
37+
elseif err isa AllocCheck.AllocCheckFailure
38+
$(Test.Returned)(false, nothing, $(QuoteNode(__source__)))
39+
else
40+
$(Test.Threw)(err, Base.current_exceptions(), $(QuoteNode(__source__)))
41+
end
42+
end
43+
test_do = $(length(broken) > 0 && esc(broken[1])) ? $(Test.do_broken_test) : $(Test.do_test)
44+
test_do(result, $ex)
45+
end
46+
end
47+
end
48+
49+
end

src/AllocCheck.jl

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,28 @@ function check_allocs(@nospecialize(func), @nospecialize(types); ignore_throw=tr
215215
return allocs
216216
end
217217

218+
function _test_noalloc end # Implemented in `ext/TestAlloc.jl`
218219

219-
export check_allocs, alloc_type, @check_allocs, AllocCheckFailure
220+
"""
221+
@test_noalloc f(args...; kwargs...) key=val ...
222+
223+
Test that `f(args...; kwargs...)` does not allocate. If executed inside a
224+
`@testset`, return a `Pass Result` if no allocations occur, a `Fail Result` if
225+
there are allocations, and a `Error Result` if any other errors occur during
226+
evaluation. If executed outside a `@testset` throw an exception instead of
227+
returning `Fail` or `Error`.
228+
229+
The `broken` and `skip` keys can be set to `true`/`false`, and behave the same
230+
as in `@test`. The `ignore_throw` key can also be set to `true`/`false`, and is
231+
passed through to `@check_allocs`.
232+
"""
233+
macro test_noalloc(expr, kws...)
234+
if length(methods(_test_noalloc)) == 0
235+
error("@test_noalloc is an extension to Test, but Test is not loaded.")
236+
end
237+
_test_noalloc(__module__, __source__, expr, kws...)
238+
end
239+
240+
export check_allocs, alloc_type, @check_allocs, AllocCheckFailure, @test_noalloc
220241

221242
end

test/runtests.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,8 @@ end
273273
@test !allunique(allocs)
274274
@test length(unique(allocs)) == 2
275275
end
276+
277+
@testset "@test_noalloc" begin
278+
@test_noalloc 1 + 1
279+
@test_noalloc rand(2, 2) * rand(2, 2) broken=true
280+
end

0 commit comments

Comments
 (0)