From d30ed0a51aca0bf988051f74f10ea4825cc8e8bb Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 14 Jun 2026 05:16:26 -0400 Subject: [PATCH] Canonicalize test units with @safetestset Wrap each independent test unit in its own `@safetestset` so it runs in a fresh module (isolation between tests + world-age safety), matching the canonical OrdinaryDiffEq structure. Core (runtests.jl else-branch): - The sampler distribution loops + their `test_samples` helper moved into a self-contained test/sampler_tests.jl, wrapped in `@safetestset "Sampler distribution tests"`. - `@testset "BigFloat support"` -> test/bigfloat_tests.jl. - `@testset "PassthroughRNG dispatch"` -> test/passthrough_rng_tests.jl. - `@testset "Allocation Tests"` keeps including alloc_tests.jl, now via `@safetestset`. Each extracted file carries its own `using`/`import` lines. QA (test/qa/qa.jl): the Aqua / JET / ExplicitImports plain `@testset` blocks become `@safetestset`s, each including a self-contained file (qa_aqua.jl / qa_jet.jl / qa_explicitimports.jl). The include() form is used so `using JET` runs before the qualified `JET.@test_opt`/`@test_call` macros are reached. The GROUP dispatch ladder and all assertions are unchanged. Adds SafeTestsets to the main test deps and to the QA env (test/qa/Project.toml, which is activated separately). Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 4 +- test/bigfloat_tests.jl | 25 +++++++ test/passthrough_rng_tests.jl | 16 +++++ test/qa/Project.toml | 2 + test/qa/qa.jl | 37 ++-------- test/qa/qa_aqua.jl | 14 ++++ test/qa/qa_explicitimports.jl | 5 ++ test/qa/qa_jet.jl | 15 ++++ test/runtests.jl | 131 +++------------------------------- test/sampler_tests.jl | 91 +++++++++++++++++++++++ 10 files changed, 186 insertions(+), 154 deletions(-) create mode 100644 test/bigfloat_tests.jl create mode 100644 test/passthrough_rng_tests.jl create mode 100644 test/qa/qa_aqua.jl create mode 100644 test/qa/qa_explicitimports.jl create mode 100644 test/qa/qa_jet.jl create mode 100644 test/sampler_tests.jl diff --git a/Project.toml b/Project.toml index a7a2e1f..f7b7aef 100644 --- a/Project.toml +++ b/Project.toml @@ -17,6 +17,7 @@ LogExpFunctions = "0.3.14, 1.0" Pkg = "1" PrecompileTools = "1.2.0" Random = "1.10" +SafeTestsets = "0.1, 1" Statistics = "1" Test = "1" julia = "1.10" @@ -28,8 +29,9 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AllocCheck", "Aqua", "Statistics", "Test", "Distributions", "JET", "ExplicitImports", "Pkg"] +test = ["AllocCheck", "Aqua", "Statistics", "Test", "Distributions", "JET", "ExplicitImports", "Pkg", "SafeTestsets"] diff --git a/test/bigfloat_tests.jl b/test/bigfloat_tests.jl new file mode 100644 index 0000000..a635052 --- /dev/null +++ b/test/bigfloat_tests.jl @@ -0,0 +1,25 @@ +using PoissonRandom +using Statistics +using Test + +@testset "count_rand with BigFloat (λ < 6)" begin + for _ in 1:100 + result = pois_rand(BigFloat(3.0)) + @test result isa Integer + @test result >= 0 + end +end +@testset "ad_rand with BigFloat (λ >= 6)" begin + for _ in 1:100 + result = pois_rand(BigFloat(15.0)) + @test result isa Integer + @test result >= 0 + end +end +@testset "statistical validity with BigFloat" begin + n = 10000 + λ = BigFloat(10.0) + samples = [pois_rand(λ) for _ in 1:n] + sample_mean = mean(samples) + @test abs(sample_mean - Float64(λ)) < 3 * sqrt(Float64(λ)) +end diff --git a/test/passthrough_rng_tests.jl b/test/passthrough_rng_tests.jl new file mode 100644 index 0000000..6321a1e --- /dev/null +++ b/test/passthrough_rng_tests.jl @@ -0,0 +1,16 @@ +using PoissonRandom +using Random: Random, UInt52Raw +using Test + +prng = PassthroughRNG() +# The CUDA.jl @device_override Random.randexp(::AbstractRNG) shadows our +# specific Random.randexp(::PassthroughRNG) on the GPU because Julia's +# OverlayMethodTable returns overlay matches without consulting the base +# table when the overlay fully covers the signature. The override body +# then calls these against PassthroughRNG; if they MethodError, kernel +# compilation fails with InvalidIRError on jl_f_throw_methoderror. +@test Random.rng_native_52(prng) === UInt64 +@test Random.rand(prng, UInt52Raw()) isa UInt64 +@test Random.rand(prng, UInt64) isa UInt64 +@test Random.rand(prng, Float32) isa Float32 +@test Random.rand(prng, Float64) isa Float64 diff --git a/test/qa/Project.toml b/test/qa/Project.toml index d5bc8f0..2daf457 100644 --- a/test/qa/Project.toml +++ b/test/qa/Project.toml @@ -4,6 +4,7 @@ ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" PoissonRandom = "e409e4f3-bfea-5376-8464-e040bb5c01ab" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [sources] @@ -14,5 +15,6 @@ Aqua = "0.8" ExplicitImports = "1.14.0" JET = "0.9, 0.10, 0.11" Random = "1.10" +SafeTestsets = "0.1, 1" Test = "1" julia = "1.10" diff --git a/test/qa/qa.jl b/test/qa/qa.jl index 7e6923e..e2076fe 100644 --- a/test/qa/qa.jl +++ b/test/qa/qa.jl @@ -1,36 +1,13 @@ -using PoissonRandom, Aqua, JET, ExplicitImports -using Random -using Test +using SafeTestsets -@testset "Aqua" begin - Aqua.find_persistent_tasks_deps(PoissonRandom) - Aqua.test_ambiguities(PoissonRandom, recursive = false) - Aqua.test_deps_compat(PoissonRandom) - Aqua.test_piracies( - PoissonRandom, - treat_as_own = [] - ) - Aqua.test_project_extras(PoissonRandom) - Aqua.test_stale_deps(PoissonRandom) - Aqua.test_unbound_args(PoissonRandom) - Aqua.test_undefined_exports(PoissonRandom) +@safetestset "Aqua" begin + include("qa_aqua.jl") end -@testset "JET static analysis" begin - @testset "Type stability" begin - JET.@test_opt target_modules = (PoissonRandom,) pois_rand(10.0) - JET.@test_opt target_modules = (PoissonRandom,) pois_rand(Random.default_rng(), 10.0) - JET.@test_opt target_modules = (PoissonRandom,) pois_rand(PassthroughRNG(), 10.0) - end - - @testset "Error analysis" begin - JET.@test_call target_modules = (PoissonRandom,) pois_rand(10.0) - JET.@test_call target_modules = (PoissonRandom,) pois_rand(Random.default_rng(), 10.0) - JET.@test_call target_modules = (PoissonRandom,) pois_rand(PassthroughRNG(), 10.0) - end +@safetestset "JET static analysis" begin + include("qa_jet.jl") end -@testset "ExplicitImports" begin - @test check_no_implicit_imports(PoissonRandom) === nothing - @test check_no_stale_explicit_imports(PoissonRandom) === nothing +@safetestset "ExplicitImports" begin + include("qa_explicitimports.jl") end diff --git a/test/qa/qa_aqua.jl b/test/qa/qa_aqua.jl new file mode 100644 index 0000000..9803d56 --- /dev/null +++ b/test/qa/qa_aqua.jl @@ -0,0 +1,14 @@ +using PoissonRandom, Aqua +using Test + +Aqua.find_persistent_tasks_deps(PoissonRandom) +Aqua.test_ambiguities(PoissonRandom, recursive = false) +Aqua.test_deps_compat(PoissonRandom) +Aqua.test_piracies( + PoissonRandom, + treat_as_own = [] +) +Aqua.test_project_extras(PoissonRandom) +Aqua.test_stale_deps(PoissonRandom) +Aqua.test_unbound_args(PoissonRandom) +Aqua.test_undefined_exports(PoissonRandom) diff --git a/test/qa/qa_explicitimports.jl b/test/qa/qa_explicitimports.jl new file mode 100644 index 0000000..44fd9f0 --- /dev/null +++ b/test/qa/qa_explicitimports.jl @@ -0,0 +1,5 @@ +using PoissonRandom, ExplicitImports +using Test + +@test check_no_implicit_imports(PoissonRandom) === nothing +@test check_no_stale_explicit_imports(PoissonRandom) === nothing diff --git a/test/qa/qa_jet.jl b/test/qa/qa_jet.jl new file mode 100644 index 0000000..0f9c2c6 --- /dev/null +++ b/test/qa/qa_jet.jl @@ -0,0 +1,15 @@ +using PoissonRandom, JET +using Random +using Test + +@testset "Type stability" begin + JET.@test_opt target_modules = (PoissonRandom,) pois_rand(10.0) + JET.@test_opt target_modules = (PoissonRandom,) pois_rand(Random.default_rng(), 10.0) + JET.@test_opt target_modules = (PoissonRandom,) pois_rand(PassthroughRNG(), 10.0) +end + +@testset "Error analysis" begin + JET.@test_call target_modules = (PoissonRandom,) pois_rand(10.0) + JET.@test_call target_modules = (PoissonRandom,) pois_rand(Random.default_rng(), 10.0) + JET.@test_call target_modules = (PoissonRandom,) pois_rand(PassthroughRNG(), 10.0) +end diff --git a/test/runtests.jl b/test/runtests.jl index 9c8fded..6deaf35 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ using PoissonRandom import Distributions using Test, Statistics +using SafeTestsets const GROUP = get(ENV, "GROUP", "All") @@ -11,135 +12,19 @@ if GROUP == "QA" Pkg.instantiate() include(joinpath(@__DIR__, "qa", "qa.jl")) else - n_tsamples = 10^5 - - function test_samples( - rand_func, - distr::Distributions.DiscreteUnivariateDistribution, - n::Int; # number of samples to generate - q::Float64 = 1.0e-8, # confidence interval, 1 - q as confidence - verbose::Bool = false - ) # show intermediate info (for debugging) - - # The basic idea - # ------------------ - # Generate n samples, and count the occurrences of each value within a reasonable range. - # For each distinct value, it computes an confidence interval of the counts - # and checks whether the count is within this interval. - # - # If the distribution has a bounded range, it also checks whether - # the samples are all within this range. - # - # By setting a small q, we ensure that failure of the tests rarely - # happen in practice. - # - λ = distr.λ - n > 1 || error("The number of samples must be greater than 1.") - 0.0 < q < 0.1 || error("The value of q must be within the open interval (0.0, 0.1).") - - # determine the range of values to examine - vmin = minimum(distr) - vmax = maximum(distr) - - rmin = floor(Int, quantile(distr, 0.00001))::Int - rmax = floor(Int, quantile(distr, 0.99999))::Int - m = rmax - rmin + 1 # length of the range - p0 = Distributions.pdf.((distr,), rmin:rmax) # reference probability masses - @assert length(p0) == m - - # determine confidence intervals for counts: - # with probability q, the count will be out of this interval. - # - clb = Vector{Int}(undef, m) - cub = Vector{Int}(undef, m) - for i in 1:m - bp = Distributions.Binomial(n, p0[i]) - clb[i] = floor(Int, quantile(bp, q / 2)) - cub[i] = ceil(Int, Distributions.cquantile(bp, q / 2)) - @assert cub[i] >= clb[i] - end - - # generate samples - samples = [rand_func(λ) for i in 1:n] - @assert length(samples) == n - - # scan samples and get counts - cnts = zeros(Int, m) - for i in 1:n - @inbounds si = samples[i] - if rmin <= si <= rmax - cnts[si - rmin + 1] += 1 - else - vmin <= si <= vmax || - error("Sample value out of valid range.") - end - end - - # check the counts - for i in 1:m - verbose && println("v = $(rmin + i - 1) ==> ($(clb[i]), $(cub[i])): $(cnts[i])") - clb[i] <= cnts[i] <= cub[i] || - error("The counts are out of the confidence interval.") - end - return samples - end - - println("testing count random sampler") - for λ in [0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 15.0, 20.0, 30.0] - test_samples(PoissonRandom.count_rand, Distributions.Poisson(λ), n_tsamples) - end - - println("testing ad random sampler") - for λ in [5.0, 10.0, 15.0, 20.0, 30.0] - test_samples(PoissonRandom.ad_rand, Distributions.Poisson(λ), n_tsamples) - end - - println("testing mixed random sampler") - for λ in [5.0, 10.0, 15.0, 20.0, 30.0] - test_samples(pois_rand, Distributions.Poisson(λ), n_tsamples) + @safetestset "Sampler distribution tests" begin + include("sampler_tests.jl") end - @testset "BigFloat support" begin - @testset "count_rand with BigFloat (λ < 6)" begin - for _ in 1:100 - result = pois_rand(BigFloat(3.0)) - @test result isa Integer - @test result >= 0 - end - end - @testset "ad_rand with BigFloat (λ >= 6)" begin - for _ in 1:100 - result = pois_rand(BigFloat(15.0)) - @test result isa Integer - @test result >= 0 - end - end - @testset "statistical validity with BigFloat" begin - n = 10000 - λ = BigFloat(10.0) - samples = [pois_rand(λ) for _ in 1:n] - sample_mean = mean(samples) - @test abs(sample_mean - Float64(λ)) < 3 * sqrt(Float64(λ)) - end + @safetestset "BigFloat support" begin + include("bigfloat_tests.jl") end - @testset "PassthroughRNG dispatch" begin - using Random: Random, UInt52Raw - prng = PassthroughRNG() - # The CUDA.jl @device_override Random.randexp(::AbstractRNG) shadows our - # specific Random.randexp(::PassthroughRNG) on the GPU because Julia's - # OverlayMethodTable returns overlay matches without consulting the base - # table when the overlay fully covers the signature. The override body - # then calls these against PassthroughRNG; if they MethodError, kernel - # compilation fails with InvalidIRError on jl_f_throw_methoderror. - @test Random.rng_native_52(prng) === UInt64 - @test Random.rand(prng, UInt52Raw()) isa UInt64 - @test Random.rand(prng, UInt64) isa UInt64 - @test Random.rand(prng, Float32) isa Float32 - @test Random.rand(prng, Float64) isa Float64 + @safetestset "PassthroughRNG dispatch" begin + include("passthrough_rng_tests.jl") end - @testset "Allocation Tests" begin + @safetestset "Allocation Tests" begin include("alloc_tests.jl") end end diff --git a/test/sampler_tests.jl b/test/sampler_tests.jl new file mode 100644 index 0000000..08c18da --- /dev/null +++ b/test/sampler_tests.jl @@ -0,0 +1,91 @@ +using PoissonRandom +import Distributions +using Statistics + +n_tsamples = 10^5 + +function test_samples( + rand_func, + distr::Distributions.DiscreteUnivariateDistribution, + n::Int; # number of samples to generate + q::Float64 = 1.0e-8, # confidence interval, 1 - q as confidence + verbose::Bool = false + ) # show intermediate info (for debugging) + + # The basic idea + # ------------------ + # Generate n samples, and count the occurrences of each value within a reasonable range. + # For each distinct value, it computes an confidence interval of the counts + # and checks whether the count is within this interval. + # + # If the distribution has a bounded range, it also checks whether + # the samples are all within this range. + # + # By setting a small q, we ensure that failure of the tests rarely + # happen in practice. + # + λ = distr.λ + n > 1 || error("The number of samples must be greater than 1.") + 0.0 < q < 0.1 || error("The value of q must be within the open interval (0.0, 0.1).") + + # determine the range of values to examine + vmin = minimum(distr) + vmax = maximum(distr) + + rmin = floor(Int, quantile(distr, 0.00001))::Int + rmax = floor(Int, quantile(distr, 0.99999))::Int + m = rmax - rmin + 1 # length of the range + p0 = Distributions.pdf.((distr,), rmin:rmax) # reference probability masses + @assert length(p0) == m + + # determine confidence intervals for counts: + # with probability q, the count will be out of this interval. + # + clb = Vector{Int}(undef, m) + cub = Vector{Int}(undef, m) + for i in 1:m + bp = Distributions.Binomial(n, p0[i]) + clb[i] = floor(Int, quantile(bp, q / 2)) + cub[i] = ceil(Int, Distributions.cquantile(bp, q / 2)) + @assert cub[i] >= clb[i] + end + + # generate samples + samples = [rand_func(λ) for i in 1:n] + @assert length(samples) == n + + # scan samples and get counts + cnts = zeros(Int, m) + for i in 1:n + @inbounds si = samples[i] + if rmin <= si <= rmax + cnts[si - rmin + 1] += 1 + else + vmin <= si <= vmax || + error("Sample value out of valid range.") + end + end + + # check the counts + for i in 1:m + verbose && println("v = $(rmin + i - 1) ==> ($(clb[i]), $(cub[i])): $(cnts[i])") + clb[i] <= cnts[i] <= cub[i] || + error("The counts are out of the confidence interval.") + end + return samples +end + +println("testing count random sampler") +for λ in [0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 15.0, 20.0, 30.0] + test_samples(PoissonRandom.count_rand, Distributions.Poisson(λ), n_tsamples) +end + +println("testing ad random sampler") +for λ in [5.0, 10.0, 15.0, 20.0, 30.0] + test_samples(PoissonRandom.ad_rand, Distributions.Poisson(λ), n_tsamples) +end + +println("testing mixed random sampler") +for λ in [5.0, 10.0, 15.0, 20.0, 30.0] + test_samples(pois_rand, Distributions.Poisson(λ), n_tsamples) +end