diff --git a/test/alloc_tests.jl b/test/alloc_tests.jl index 9fa5f29..f8e6935 100644 --- a/test/alloc_tests.jl +++ b/test/alloc_tests.jl @@ -1,49 +1,62 @@ using AllocCheck using PoissonRandom using Random +using Test + +# AllocCheck's static LLVM-IR analysis is explicitly documented as not guaranteed +# stable across Julia versions, and on Julia prereleases the in-flux codegen makes +# it emit architecture-dependent false positives: on 1.13.0-rc1 the aarch64 path +# flags even `procf` (pure scalar Float64 arithmetic returning an `NTuple{4,Float64}`, +# which cannot heap-allocate) as allocating, while x86_64 reports zero. The runtime +# `@allocated == 0` check below is the ground-truth regression guard and is exercised +# on every version/architecture; the static `@check_allocs` guard is additionally run +# only on released Julia, where its result is reliable. +const RELIABLE_STATIC_ALLOC_CHECK = isempty(VERSION.prerelease) + +# Assert `f(args...)` performs zero heap allocations at runtime (the real guard), and +# additionally pass it through AllocCheck's static analysis on released Julia. +macro test_zero_allocs(call) + @assert call.head == :call + f = call.args[1] + args = call.args[2:end] + return quote + local f = $(esc(f)) + local args = ($(map(esc, args)...),) + f(args...) # warm up / compile + @test (@allocated f(args...)) == 0 + if RELIABLE_STATIC_ALLOC_CHECK + local checked = AllocCheck.check_allocs(f, map(typeof, args)) + @test isempty(checked) + end + end +end @testset "AllocCheck - Zero Allocations" begin + rng = Random.default_rng() + passthrough = PassthroughRNG() + @testset "count_rand" begin - @check_allocs function test_count_rand(rng::TaskLocalRNG, λ::Float64) - PoissonRandom.count_rand(rng, λ) - end - rng = Random.default_rng() - @test test_count_rand(rng, 2.0) isa Int - @test test_count_rand(rng, 5.0) isa Int + @test_zero_allocs PoissonRandom.count_rand(rng, 2.0) + @test_zero_allocs PoissonRandom.count_rand(rng, 5.0) end @testset "ad_rand" begin - @check_allocs function test_ad_rand(rng::TaskLocalRNG, λ::Float64) - PoissonRandom.ad_rand(rng, λ) - end - rng = Random.default_rng() - @test test_ad_rand(rng, 10.0) isa Int - @test test_ad_rand(rng, 50.0) isa Int + @test_zero_allocs PoissonRandom.ad_rand(rng, 10.0) + @test_zero_allocs PoissonRandom.ad_rand(rng, 50.0) end @testset "pois_rand" begin - @check_allocs function test_pois_rand(rng::TaskLocalRNG, λ::Float64) - pois_rand(rng, λ) - end - rng = Random.default_rng() - @test test_pois_rand(rng, 2.0) isa Int - @test test_pois_rand(rng, 10.0) isa Int + @test_zero_allocs pois_rand(rng, 2.0) + @test_zero_allocs pois_rand(rng, 10.0) end @testset "pois_rand with PassthroughRNG" begin - @check_allocs function test_pois_rand_passthrough(rng::PassthroughRNG, λ::Float64) - pois_rand(rng, λ) - end - passthrough = PassthroughRNG() - @test test_pois_rand_passthrough(passthrough, 2.0) isa Int - @test test_pois_rand_passthrough(passthrough, 10.0) isa Int + @test_zero_allocs pois_rand(passthrough, 2.0) + @test_zero_allocs pois_rand(passthrough, 10.0) end @testset "procf" begin - @check_allocs function test_procf(λ::Float64, K::Int, s::Float64) - PoissonRandom.procf(λ, K, s) - end - @test test_procf(10.0, 5, 3.162) isa NTuple{4, Float64} - @test test_procf(10.0, 15, 3.162) isa NTuple{4, Float64} + @test_zero_allocs PoissonRandom.procf(10.0, 5, 3.162) + @test_zero_allocs PoissonRandom.procf(10.0, 15, 3.162) end end