From 0efe3e0de02981e29119c90c59c8343391021e9a Mon Sep 17 00:00:00 2001 From: ChrisRackauckas-Claude Date: Sun, 14 Jun 2026 05:04:40 -0400 Subject: [PATCH] Canonicalize test units with @safetestset Convert the independent top-level test units to @safetestset so each runs in its own module (isolation + world-age safety), matching the canonical OrdinaryDiffEq structure. - Core group: "Core Tests" and "Analytical Solutions" inline bodies extracted verbatim into self-contained test/core_tests.jl and test/analytical_tests.jl (each carrying its own `using DiffEqFinancial, Statistics, StochasticDiffEq, Test`), then included from @safetestset units. "Interface Compatibility" (already a self-contained include) becomes @safetestset. - QA group: the three qa.jl units (Explicit Imports, JET, AllocCheck) become @safetestset; each included file already carries its own imports. The package macros (@test_opt, @check_allocs) resolve correctly because each unit is just `include(file)` and the file's `using` runs before the macro is parsed. - Add SafeTestsets to the main Project.toml [extras]/[targets].test + [compat] and to test/qa/Project.toml (QA runs in its own activated env). Behavior-preserving: identical tests, assertions, and GROUP dispatch. Nested grouping @testsets inside units left as plain @testset. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Opus 4.8 (1M context) --- Project.toml | 4 +- test/analytical_tests.jl | 171 +++++++++++++++++++++++++++++++++ test/core_tests.jl | 31 ++++++ test/qa/Project.toml | 2 + test/qa/qa.jl | 8 +- test/runtests.jl | 203 ++------------------------------------- 6 files changed, 218 insertions(+), 201 deletions(-) create mode 100644 test/analytical_tests.jl create mode 100644 test/core_tests.jl diff --git a/Project.toml b/Project.toml index 2af5536..2dfeef1 100644 --- a/Project.toml +++ b/Project.toml @@ -16,6 +16,7 @@ DiffEqNoiseProcess = "5.13.0" Distributions = "0.25.100" Pkg = "1" RandomNumbers = "1.5.3" +SafeTestsets = "0.1, 1" Statistics = "1" StochasticDiffEq = "6.65.0, 7" Test = "1" @@ -24,9 +25,10 @@ julia = "1.10" [extras] Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "Statistics", "StochasticDiffEq", "Distributions", "Pkg"] +test = ["Test", "Statistics", "StochasticDiffEq", "Distributions", "Pkg", "SafeTestsets"] diff --git a/test/analytical_tests.jl b/test/analytical_tests.jl new file mode 100644 index 0000000..4a631f0 --- /dev/null +++ b/test/analytical_tests.jl @@ -0,0 +1,171 @@ +using DiffEqFinancial, Statistics, StochasticDiffEq, Test + +@testset "Analytical Solutions" begin + @testset "GBM analytical functions" begin + μ = 0.05 + σ = 0.2 + u0 = 100.0 + t = 1.0 + + # Test at t=0 (should return initial value for mean, 0 for variance) + @test gbm_mean(μ, u0, 0.0) ≈ u0 + @test gbm_variance(μ, σ, u0, 0.0) ≈ 0.0 atol = 1.0e-15 + + # Test analytical formulas + @test gbm_mean(μ, u0, t) ≈ u0 * exp(μ * t) + @test gbm_variance(μ, σ, u0, t) ≈ u0^2 * exp(2μ * t) * (exp(σ^2 * t) - 1) + @test gbm_std(μ, σ, u0, t) ≈ sqrt(gbm_variance(μ, σ, u0, t)) + + # Test broadcasting with array of times + times = [0.0, 0.5, 1.0, 2.0] + means = gbm_mean.(μ, u0, times) + @test length(means) == 4 + @test means[1] ≈ u0 + + # Monte Carlo validation + prob = GeometricBrownianMotionProblem(μ, σ, u0, (0.0, t)) + monte_prob = EnsembleProblem(prob) + sol = solve(monte_prob, EM(); dt = 0.01, trajectories = 10000) + final_values = [sol.u[i].u[end] for i in eachindex(sol.u)] + simulated_mean = mean(final_values) + simulated_var = var(final_values) + + analytical_mean = gbm_mean(μ, u0, t) + analytical_var = gbm_variance(μ, σ, u0, t) + + # Allow 5% relative error for Monte Carlo + @test abs(simulated_mean - analytical_mean) / analytical_mean < 0.05 + @test abs(simulated_var - analytical_var) / analytical_var < 0.15 + end + + @testset "OU analytical functions" begin + a = 0.5 + r = 1.0 + σ = 0.3 + u0 = 0.5 + t = 2.0 + + # Test at t=0 + @test ou_mean(a, r, u0, 0.0) ≈ u0 + @test ou_variance(a, σ, 0.0) ≈ 0.0 atol = 1.0e-15 + + # Test analytical formulas + @test ou_mean(a, r, u0, t) ≈ r + (u0 - r) * exp(-a * t) + @test ou_variance(a, σ, t) ≈ (σ^2 / (2a)) * (1 - exp(-2a * t)) + @test ou_std(a, σ, t) ≈ sqrt(ou_variance(a, σ, t)) + + # Test stationary values + @test ou_stationary_mean(r) == r + @test ou_stationary_variance(a, σ) ≈ σ^2 / (2a) + + # Test convergence to stationary distribution + large_t = 100.0 + @test ou_mean(a, r, u0, large_t) ≈ ou_stationary_mean(r) atol = 1.0e-10 + @test ou_variance(a, σ, large_t) ≈ ou_stationary_variance(a, σ) atol = 1.0e-10 + + # Monte Carlo validation + prob = OrnsteinUhlenbeckProblem(a, r, σ, u0, (0.0, t)) + monte_prob = EnsembleProblem(prob) + sol = solve(monte_prob, EM(); dt = 0.01, trajectories = 10000) + final_values = [sol.u[i].u[end] for i in eachindex(sol.u)] + simulated_mean = mean(final_values) + simulated_var = var(final_values) + + analytical_mean = ou_mean(a, r, u0, t) + analytical_var = ou_variance(a, σ, t) + + @test abs(simulated_mean - analytical_mean) < 0.05 + @test abs(simulated_var - analytical_var) / analytical_var < 0.15 + end + + @testset "CIR analytical functions" begin + κ = 0.5 + θ = 0.04 + σ = 0.1 + u0 = 0.03 + t = 2.0 + + # Verify Feller condition for test parameters + @test 2κ * θ >= σ^2 # Should satisfy Feller condition + + # Test at t=0 + @test cir_mean(κ, θ, u0, 0.0) ≈ u0 + @test cir_variance(κ, θ, σ, u0, 0.0) ≈ 0.0 atol = 1.0e-15 + + # Test analytical formulas + exp_κt = exp(-κ * t) + exp_2κt = exp(-2κ * t) + expected_mean = θ + (u0 - θ) * exp_κt + expected_var = u0 * (σ^2 / κ) * (exp_κt - exp_2κt) + + (θ * σ^2 / (2κ)) * (1 - exp_κt)^2 + + @test cir_mean(κ, θ, u0, t) ≈ expected_mean + @test cir_variance(κ, θ, σ, u0, t) ≈ expected_var + @test cir_std(κ, θ, σ, u0, t) ≈ sqrt(expected_var) + + # Test stationary values + @test cir_stationary_mean(θ) == θ + @test cir_stationary_variance(κ, θ, σ) ≈ θ * σ^2 / (2κ) + + # Test convergence to stationary distribution + large_t = 100.0 + @test cir_mean(κ, θ, u0, large_t) ≈ cir_stationary_mean(θ) atol = 1.0e-10 + + # Monte Carlo validation + prob = CIRProblem(κ, θ, σ, u0, (0.0, t)) + monte_prob = EnsembleProblem(prob) + sol = solve(monte_prob, EM(); dt = 0.001, trajectories = 10000) + final_values = [sol.u[i].u[end] for i in eachindex(sol.u)] + simulated_mean = mean(final_values) + simulated_var = var(final_values) + + analytical_mean = cir_mean(κ, θ, u0, t) + analytical_var = cir_variance(κ, θ, σ, u0, t) + + @test abs(simulated_mean - analytical_mean) / analytical_mean < 0.05 + @test abs(simulated_var - analytical_var) / analytical_var < 0.2 + end + + @testset "Black-Scholes log-price functions" begin + r = 0.05 + σ = 0.2 + u0 = log(100.0) # log of initial price + t = 1.0 + + # Test at t=0 + @test bs_log_mean(r, σ, u0, 0.0) ≈ u0 + @test bs_log_variance(σ, 0.0) ≈ 0.0 atol = 1.0e-15 + + # Test analytical formulas + @test bs_log_mean(r, σ, u0, t) ≈ u0 + (r - σ^2 / 2) * t + @test bs_log_variance(σ, t) ≈ σ^2 * t + @test bs_log_std(σ, t) ≈ σ * sqrt(t) + + # Test broadcasting + times = [0.25, 0.5, 1.0] + variances = bs_log_variance.(σ, times) + @test variances ≈ σ^2 .* times + end + + @testset "Heston analytical functions" begin + μ = 0.05 + κ = 2.0 + Θ = 0.04 + σ = 0.3 + u0_S = 100.0 + u0_v = 0.04 + t = 1.0 + + # Test Heston mean (should match GBM mean since volatility has zero mean increment) + @test heston_mean(μ, u0_S, t) ≈ u0_S * exp(μ * t) + @test heston_mean(μ, u0_S, 0.0) ≈ u0_S + + # Test variance process mean (should match CIR mean) + @test heston_variance_mean(κ, Θ, u0_v, t) ≈ cir_mean(κ, Θ, u0_v, t) + @test heston_variance_mean(κ, Θ, u0_v, 0.0) ≈ u0_v + + # Test variance of variance process (should match CIR variance) + @test heston_variance_variance(κ, Θ, σ, u0_v, t) ≈ + cir_variance(κ, Θ, σ, u0_v, t) + end +end diff --git a/test/core_tests.jl b/test/core_tests.jl new file mode 100644 index 0000000..1a5f46b --- /dev/null +++ b/test/core_tests.jl @@ -0,0 +1,31 @@ +using DiffEqFinancial, Statistics, StochasticDiffEq, Test + +@testset "Core Tests" begin + u0 = [1.0; 0.5] + σ = 0.25 + prob = HestonProblem(1.0, 1.0, σ, 1.0, 1.0, u0, (0.0, 1.0)) + sol = solve(prob, SRIW1(), adaptive = false, dt = 1 / 100) + + prob = BlackScholesProblem((t) -> t^2, (t, u) -> 1.0, σ, 0.5, (0.0, 1.0)) + sol = solve(prob, SRIW1()) + + r = 0.03 + sigma = 0.2 + S0 = 100.0 + t = 0.0 + T = 1.0 + days = 252 + dt = 1 / days + + prob = GeometricBrownianMotionProblem(r, sigma, S0, (t, T)) + sol = solve(prob, EM(); dt = dt) + monte_prob = EnsembleProblem(prob) + sol = solve(monte_prob, EM(); dt = dt, trajectories = 1000000) + us = [sol.u[i].u for i in eachindex(sol.u)] + simulated = mean(us) + + tsteps = collect(0:dt:T) + expected = S0 * exp.(r * tsteps) + testerr = sum(abs2.(simulated .- expected)) + @test testerr < 2.5e-1 +end diff --git a/test/qa/Project.toml b/test/qa/Project.toml index 16f366a..67515c3 100644 --- a/test/qa/Project.toml +++ b/test/qa/Project.toml @@ -3,6 +3,7 @@ AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a" DiffEqFinancial = "5a0ffddc-d203-54b0-88ba-2c03c0fc2e67" ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [sources] @@ -13,5 +14,6 @@ AllocCheck = "0.2" DiffEqFinancial = "2.10.0" ExplicitImports = "1.14" JET = "0.9, 0.10, 0.11" +SafeTestsets = "0.1, 1" Test = "1.10" julia = "1.10" diff --git a/test/qa/qa.jl b/test/qa/qa.jl index 190cbe9..7bd3865 100644 --- a/test/qa/qa.jl +++ b/test/qa/qa.jl @@ -1,9 +1,11 @@ -@testset "Explicit Imports" begin +using SafeTestsets + +@safetestset "Explicit Imports" begin include("explicit_imports.jl") end -@testset "JET Tests" begin +@safetestset "JET Tests" begin include("jet_tests.jl") end -@testset "AllocCheck - Zero Allocations" begin +@safetestset "AllocCheck - Zero Allocations" begin include("alloc_tests.jl") end diff --git a/test/runtests.jl b/test/runtests.jl index f09a859..0164b22 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,5 @@ using Pkg +using SafeTestsets using Test using DiffEqFinancial, Statistics, StochasticDiffEq @@ -6,209 +7,17 @@ const GROUP = get(ENV, "GROUP", "All") if GROUP == "All" || GROUP == "Core" @testset "DiffEqFinancial.jl" begin - @testset "Core Tests" begin - u0 = [1.0; 0.5] - σ = 0.25 - prob = HestonProblem(1.0, 1.0, σ, 1.0, 1.0, u0, (0.0, 1.0)) - sol = solve(prob, SRIW1(), adaptive = false, dt = 1 / 100) - - prob = BlackScholesProblem((t) -> t^2, (t, u) -> 1.0, σ, 0.5, (0.0, 1.0)) - sol = solve(prob, SRIW1()) - - r = 0.03 - sigma = 0.2 - S0 = 100.0 - t = 0.0 - T = 1.0 - days = 252 - dt = 1 / days - - prob = GeometricBrownianMotionProblem(r, sigma, S0, (t, T)) - sol = solve(prob, EM(); dt = dt) - monte_prob = EnsembleProblem(prob) - sol = solve(monte_prob, EM(); dt = dt, trajectories = 1000000) - us = [sol.u[i].u for i in eachindex(sol.u)] - simulated = mean(us) - - tsteps = collect(0:dt:T) - expected = S0 * exp.(r * tsteps) - testerr = sum(abs2.(simulated .- expected)) - @test testerr < 2.5e-1 + @safetestset "Core Tests" begin + include("core_tests.jl") end # Interface tests for type genericity - @testset "Interface Compatibility" begin + @safetestset "Interface Compatibility" begin include("interface_tests.jl") end - @testset "Analytical Solutions" begin - @testset "GBM analytical functions" begin - μ = 0.05 - σ = 0.2 - u0 = 100.0 - t = 1.0 - - # Test at t=0 (should return initial value for mean, 0 for variance) - @test gbm_mean(μ, u0, 0.0) ≈ u0 - @test gbm_variance(μ, σ, u0, 0.0) ≈ 0.0 atol = 1.0e-15 - - # Test analytical formulas - @test gbm_mean(μ, u0, t) ≈ u0 * exp(μ * t) - @test gbm_variance(μ, σ, u0, t) ≈ u0^2 * exp(2μ * t) * (exp(σ^2 * t) - 1) - @test gbm_std(μ, σ, u0, t) ≈ sqrt(gbm_variance(μ, σ, u0, t)) - - # Test broadcasting with array of times - times = [0.0, 0.5, 1.0, 2.0] - means = gbm_mean.(μ, u0, times) - @test length(means) == 4 - @test means[1] ≈ u0 - - # Monte Carlo validation - prob = GeometricBrownianMotionProblem(μ, σ, u0, (0.0, t)) - monte_prob = EnsembleProblem(prob) - sol = solve(monte_prob, EM(); dt = 0.01, trajectories = 10000) - final_values = [sol.u[i].u[end] for i in eachindex(sol.u)] - simulated_mean = mean(final_values) - simulated_var = var(final_values) - - analytical_mean = gbm_mean(μ, u0, t) - analytical_var = gbm_variance(μ, σ, u0, t) - - # Allow 5% relative error for Monte Carlo - @test abs(simulated_mean - analytical_mean) / analytical_mean < 0.05 - @test abs(simulated_var - analytical_var) / analytical_var < 0.15 - end - - @testset "OU analytical functions" begin - a = 0.5 - r = 1.0 - σ = 0.3 - u0 = 0.5 - t = 2.0 - - # Test at t=0 - @test ou_mean(a, r, u0, 0.0) ≈ u0 - @test ou_variance(a, σ, 0.0) ≈ 0.0 atol = 1.0e-15 - - # Test analytical formulas - @test ou_mean(a, r, u0, t) ≈ r + (u0 - r) * exp(-a * t) - @test ou_variance(a, σ, t) ≈ (σ^2 / (2a)) * (1 - exp(-2a * t)) - @test ou_std(a, σ, t) ≈ sqrt(ou_variance(a, σ, t)) - - # Test stationary values - @test ou_stationary_mean(r) == r - @test ou_stationary_variance(a, σ) ≈ σ^2 / (2a) - - # Test convergence to stationary distribution - large_t = 100.0 - @test ou_mean(a, r, u0, large_t) ≈ ou_stationary_mean(r) atol = 1.0e-10 - @test ou_variance(a, σ, large_t) ≈ ou_stationary_variance(a, σ) atol = 1.0e-10 - - # Monte Carlo validation - prob = OrnsteinUhlenbeckProblem(a, r, σ, u0, (0.0, t)) - monte_prob = EnsembleProblem(prob) - sol = solve(monte_prob, EM(); dt = 0.01, trajectories = 10000) - final_values = [sol.u[i].u[end] for i in eachindex(sol.u)] - simulated_mean = mean(final_values) - simulated_var = var(final_values) - - analytical_mean = ou_mean(a, r, u0, t) - analytical_var = ou_variance(a, σ, t) - - @test abs(simulated_mean - analytical_mean) < 0.05 - @test abs(simulated_var - analytical_var) / analytical_var < 0.15 - end - - @testset "CIR analytical functions" begin - κ = 0.5 - θ = 0.04 - σ = 0.1 - u0 = 0.03 - t = 2.0 - - # Verify Feller condition for test parameters - @test 2κ * θ >= σ^2 # Should satisfy Feller condition - - # Test at t=0 - @test cir_mean(κ, θ, u0, 0.0) ≈ u0 - @test cir_variance(κ, θ, σ, u0, 0.0) ≈ 0.0 atol = 1.0e-15 - - # Test analytical formulas - exp_κt = exp(-κ * t) - exp_2κt = exp(-2κ * t) - expected_mean = θ + (u0 - θ) * exp_κt - expected_var = u0 * (σ^2 / κ) * (exp_κt - exp_2κt) + - (θ * σ^2 / (2κ)) * (1 - exp_κt)^2 - - @test cir_mean(κ, θ, u0, t) ≈ expected_mean - @test cir_variance(κ, θ, σ, u0, t) ≈ expected_var - @test cir_std(κ, θ, σ, u0, t) ≈ sqrt(expected_var) - - # Test stationary values - @test cir_stationary_mean(θ) == θ - @test cir_stationary_variance(κ, θ, σ) ≈ θ * σ^2 / (2κ) - - # Test convergence to stationary distribution - large_t = 100.0 - @test cir_mean(κ, θ, u0, large_t) ≈ cir_stationary_mean(θ) atol = 1.0e-10 - - # Monte Carlo validation - prob = CIRProblem(κ, θ, σ, u0, (0.0, t)) - monte_prob = EnsembleProblem(prob) - sol = solve(monte_prob, EM(); dt = 0.001, trajectories = 10000) - final_values = [sol.u[i].u[end] for i in eachindex(sol.u)] - simulated_mean = mean(final_values) - simulated_var = var(final_values) - - analytical_mean = cir_mean(κ, θ, u0, t) - analytical_var = cir_variance(κ, θ, σ, u0, t) - - @test abs(simulated_mean - analytical_mean) / analytical_mean < 0.05 - @test abs(simulated_var - analytical_var) / analytical_var < 0.2 - end - - @testset "Black-Scholes log-price functions" begin - r = 0.05 - σ = 0.2 - u0 = log(100.0) # log of initial price - t = 1.0 - - # Test at t=0 - @test bs_log_mean(r, σ, u0, 0.0) ≈ u0 - @test bs_log_variance(σ, 0.0) ≈ 0.0 atol = 1.0e-15 - - # Test analytical formulas - @test bs_log_mean(r, σ, u0, t) ≈ u0 + (r - σ^2 / 2) * t - @test bs_log_variance(σ, t) ≈ σ^2 * t - @test bs_log_std(σ, t) ≈ σ * sqrt(t) - - # Test broadcasting - times = [0.25, 0.5, 1.0] - variances = bs_log_variance.(σ, times) - @test variances ≈ σ^2 .* times - end - - @testset "Heston analytical functions" begin - μ = 0.05 - κ = 2.0 - Θ = 0.04 - σ = 0.3 - u0_S = 100.0 - u0_v = 0.04 - t = 1.0 - - # Test Heston mean (should match GBM mean since volatility has zero mean increment) - @test heston_mean(μ, u0_S, t) ≈ u0_S * exp(μ * t) - @test heston_mean(μ, u0_S, 0.0) ≈ u0_S - - # Test variance process mean (should match CIR mean) - @test heston_variance_mean(κ, Θ, u0_v, t) ≈ cir_mean(κ, Θ, u0_v, t) - @test heston_variance_mean(κ, Θ, u0_v, 0.0) ≈ u0_v - - # Test variance of variance process (should match CIR variance) - @test heston_variance_variance(κ, Θ, σ, u0_v, t) ≈ - cir_variance(κ, Θ, σ, u0_v, t) - end + @safetestset "Analytical Solutions" begin + include("analytical_tests.jl") end end end