Skip to content

Commit 373a6a8

Browse files
giordanostevengj
authored andcommitted
[Test] Print RNG of a failed testset and add option to set it (#56260)
Also, add a keyword option to `@testset` to let users override the seed used there, to make testsets more replicable. To give you a taster of what this PR enables: ``` julia> using Random, Test julia> @testset begin @test rand() == 0 end; test set: Test Failed at REPL[2]:2 Expression: rand() == 0 Evaluated: 0.559472630416976 == 0 Stacktrace: [1] top-level scope @ REPL[2]:2 [2] macro expansion @ ~/repo/julia/usr/share/julia/stdlib/v1.12/Test/src/Test.jl:1713 [inlined] [3] macro expansion @ REPL[2]:2 [inlined] [4] macro expansion @ ~/repo/julia/usr/share/julia/stdlib/v1.12/Test/src/Test.jl:679 [inlined] Test Summary: | Fail Total Time test set | 1 1 0.9s ERROR: Some tests did not pass: 0 passed, 1 failed, 0 errored, 0 broken. Random seed for this testset: Xoshiro(0x2e026445595ed28e, 0x07bb81ac4c54926d, 0x83d7d70843e8bad6, 0xdbef927d150af80b, 0xdbf91ddf2534f850) julia> @testset rng=Xoshiro(0x2e026445595ed28e, 0x07bb81ac4c54926d, 0x83d7d70843e8bad6, 0xdbef927d150af80b, 0xdbf91ddf2534f850) begin @test rand() == 0.559472630416976 end; Test Summary: | Pass Total Time test set | 1 1 0.0s ``` This also works with nested testsets, and testsets on for loops: ``` julia> @testset rng=Xoshiro(0xc380f460355639ee, 0xb39bc754b7d63bbf, 0x1551dbcfb5ed5668, 0x71ab5a18fec21a25, 0x649d0c1be1ca5436) "Outer" begin @test rand() == 0.0004120194925605336 @testset rng=Xoshiro(0xee97f5b53f7cdc49, 0x480ac387b0527d3d, 0x614b416502a9e0f5, 0x5250cb36e4a4ceb1, 0xed6615c59e475fa0) "Inner: $(i)" for i in 1:10 @test rand() == 0.39321938407066637 end end; Test Summary: | Pass Total Time Outer | 11 11 0.0s ``` Being able to see what was the seed inside a testset and being able to set it afterwards should make replicating test failures which only depend on the state of the RNG much easier to debug.
1 parent 7fb17de commit 373a6a8

File tree

3 files changed

+86
-18
lines changed

3 files changed

+86
-18
lines changed

NEWS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,15 @@ Standard library changes
183183

184184
#### Test
185185

186+
* A failing `DefaultTestSet` now prints to screen the random number generator (RNG) of the failed test, to help reproducing a stochastic failure which only depends on the state of the RNG.
187+
It is also possible seed a test set by passing the `rng` keyword argument to `@testset`:
188+
```julia
189+
using Test, Random
190+
@testset rng=Xoshiro(0x2e026445595ed28e, 0x07bb81ac4c54926d, 0x83d7d70843e8bad6, 0xdbef927d150af80b, 0xdbf91ddf2534f850) begin
191+
@test rand() == 0.559472630416976
192+
end
193+
```
194+
186195
#### Dates
187196

188197
#### Statistics

stdlib/Test/src/Test.jl

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,8 +1071,9 @@ mutable struct DefaultTestSet <: AbstractTestSet
10711071
time_end::Union{Float64,Nothing}
10721072
failfast::Bool
10731073
file::Union{String,Nothing}
1074+
rng::Union{Nothing,AbstractRNG}
10741075
end
1075-
function DefaultTestSet(desc::AbstractString; verbose::Bool = false, showtiming::Bool = true, failfast::Union{Nothing,Bool} = nothing, source = nothing)
1076+
function DefaultTestSet(desc::AbstractString; verbose::Bool = false, showtiming::Bool = true, failfast::Union{Nothing,Bool} = nothing, source = nothing, rng = nothing)
10761077
if isnothing(failfast)
10771078
# pass failfast state into child testsets
10781079
parent_ts = get_testset()
@@ -1082,7 +1083,7 @@ function DefaultTestSet(desc::AbstractString; verbose::Bool = false, showtiming:
10821083
failfast = false
10831084
end
10841085
end
1085-
return DefaultTestSet(String(desc)::String, [], 0, false, verbose, showtiming, time(), nothing, failfast, extract_file(source))
1086+
return DefaultTestSet(String(desc)::String, [], 0, false, verbose, showtiming, time(), nothing, failfast, extract_file(source), rng)
10861087
end
10871088
extract_file(source::LineNumberNode) = extract_file(source.file)
10881089
extract_file(file::Symbol) = string(file)
@@ -1219,6 +1220,13 @@ function print_test_results(ts::AbstractTestSet, depth_pad=0)
12191220
println()
12201221
# Recursively print a summary at every level
12211222
print_counts(ts, depth_pad, align, pass_width, fail_width, error_width, broken_width, total_width, duration_width, timing)
1223+
# Print the RNG of the outer testset if there are failures
1224+
if total != total_pass + total_broken
1225+
rng = get_rng(ts)
1226+
if !isnothing(rng)
1227+
println("RNG of the outermost testset: ", rng)
1228+
end
1229+
end
12221230
end
12231231

12241232

@@ -1290,6 +1298,24 @@ function filter_errors(ts::DefaultTestSet)
12901298
efs
12911299
end
12921300

1301+
"""
1302+
Test.get_rng(ts::AbstractTestSet) -> Union{Nothing,AbstractRNG}
1303+
1304+
Return the global random number generator (RNG) associated to the input testset `ts`.
1305+
If no RNG is associated to it, return `nothing`.
1306+
"""
1307+
get_rng(::AbstractTestSet) = nothing
1308+
get_rng(ts::DefaultTestSet) = ts.rng
1309+
"""
1310+
Test.set_rng!(ts::AbstractTestSet, rng::AbstractRNG) -> AbstractRNG
1311+
1312+
Set the global random number generator (RNG) associated to the input testset `ts` to `rng`.
1313+
If no RNG is associated to it, do nothing.
1314+
In any case, always return the input `rng`.
1315+
"""
1316+
set_rng!(::AbstractTestSet, rng::AbstractRNG) = rng
1317+
set_rng!(ts::DefaultTestSet, rng::AbstractRNG) = ts.rng = rng
1318+
12931319
"""
12941320
TestCounts
12951321
@@ -1494,21 +1520,27 @@ along with a summary of the test results.
14941520
Any custom testset type (subtype of `AbstractTestSet`) can be given and it will
14951521
also be used for any nested `@testset` invocations. The given options are only
14961522
applied to the test set where they are given. The default test set type
1497-
accepts three boolean options:
1498-
- `verbose`: if `true`, the result summary of the nested testsets is shown even
1523+
accepts the following options:
1524+
- `verbose::Bool`: if `true`, the result summary of the nested testsets is shown even
14991525
when they all pass (the default is `false`).
1500-
- `showtiming`: if `true`, the duration of each displayed testset is shown
1526+
- `showtiming::Bool`: if `true`, the duration of each displayed testset is shown
15011527
(the default is `true`).
1502-
- `failfast`: if `true`, any test failure or error will cause the testset and any
1528+
- `failfast::Bool`: if `true`, any test failure or error will cause the testset and any
15031529
child testsets to return immediately (the default is `false`).
15041530
This can also be set globally via the env var `JULIA_TEST_FAILFAST`.
1531+
- `rng::Random.AbstractRNG`: use the given random number generator (RNG) as the global one
1532+
for the testset. `rng` must be `copy!`-able. This option can be useful to locally
1533+
reproduce stochastic test failures which only depend on the state of the global RNG.
15051534
15061535
!!! compat "Julia 1.8"
15071536
`@testset test_func()` requires at least Julia 1.8.
15081537
15091538
!!! compat "Julia 1.9"
15101539
`failfast` requires at least Julia 1.9.
15111540
1541+
!!! compat "Julia 1.12"
1542+
The `rng` option requires at least Julia 1.12.
1543+
15121544
The description string accepts interpolation from the loop indices.
15131545
If no description is provided, one is constructed based on the variables.
15141546
If a function call is provided, its name will be used.
@@ -1521,13 +1553,19 @@ method, which by default will return a list of the testset objects used in
15211553
each iteration.
15221554
15231555
Before the execution of the body of a `@testset`, there is an implicit
1524-
call to `Random.seed!(seed)` where `seed` is the current seed of the global RNG.
1556+
call to `copy!(Random.default_rng(), rng)` where `rng` is the RNG of the current task, or
1557+
the value of the RNG passed via the `rng` option.
15251558
Moreover, after the execution of the body, the state of the global RNG is
15261559
restored to what it was before the `@testset`. This is meant to ease
15271560
reproducibility in case of failure, and to allow seamless
15281561
re-arrangements of `@testset`s regardless of their side-effect on the
15291562
global RNG state.
15301563
1564+
!!! note "RNG of nested testsets"
1565+
Unless changed with the `rng` option, the same RNG is set at the beginning of all
1566+
nested testsets. The RNG printed to screen when a testset has failures is the global RNG of
1567+
the outermost testset even if inner testsets have different RNGs manually set by the user.
1568+
15311569
## Examples
15321570
```jldoctest; filter = r"trigonometric identities | 4 4 [0-9\\.]+s"
15331571
julia> @testset "trigonometric identities" begin
@@ -1717,9 +1755,11 @@ function testset_beginend_call(args, tests, source)
17171755
# by wrapping the body in a function
17181756
local default_rng_orig = copy(default_rng())
17191757
local tls_seed_orig = copy(Random.get_tls_seed())
1758+
local tls_seed = isnothing(get_rng(ts)) ? set_rng!(ts, tls_seed_orig) : get_rng(ts)
17201759
try
17211760
# default RNG is reset to its state from last `seed!()` to ease reproduce a failed test
1722-
copy!(Random.default_rng(), tls_seed_orig)
1761+
copy!(Random.default_rng(), tls_seed)
1762+
copy!(Random.get_tls_seed(), Random.default_rng())
17231763
let
17241764
$(esc(tests))
17251765
end
@@ -1800,10 +1840,10 @@ function testset_forloop(args, testloop, source)
18001840
finish_errored = true
18011841
push!(arr, finish(ts))
18021842
finish_errored = false
1803-
copy!(default_rng(), tls_seed_orig)
1843+
copy!(default_rng(), tls_seed)
18041844
end
18051845
ts = if ($testsettype === $DefaultTestSet) && $(isa(source, LineNumberNode))
1806-
$(testsettype)($desc; source=$(QuoteNode(source.file)), $options...)
1846+
$(testsettype)($desc; source=$(QuoteNode(source.file)), $options..., rng=tls_seed)
18071847
else
18081848
$(testsettype)($desc; $options...)
18091849
end
@@ -1825,10 +1865,12 @@ function testset_forloop(args, testloop, source)
18251865
local arr = Vector{Any}()
18261866
local first_iteration = true
18271867
local ts
1868+
local rng_option = get($(options), :rng, nothing)
18281869
local finish_errored = false
18291870
local default_rng_orig = copy(default_rng())
18301871
local tls_seed_orig = copy(Random.get_tls_seed())
1831-
copy!(Random.default_rng(), tls_seed_orig)
1872+
local tls_seed = isnothing(rng_option) ? copy(Random.get_tls_seed()) : rng_option
1873+
copy!(Random.default_rng(), tls_seed)
18321874
try
18331875
let
18341876
$(Expr(:for, Expr(:block, [esc(v) for v in loopvars]...), blk))

stdlib/Test/test/runtests.jl

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,13 +1712,13 @@ end
17121712

17131713
# this tests both the `TestCounts` parts as well as the fallback `x`s
17141714
expected = r"""
1715-
Test Summary: | Pass Fail Error Broken Total Time
1716-
outer | 3 1 1 1 6 \s*\d*.\ds
1717-
a | 1 1 \s*\d*.\ds
1718-
custom | 1 1 1 1 4 \s*?s
1719-
no-record | x x x x ? \s*?s
1720-
b | 1 1 \s*\d*.\ds
1721-
ERROR: Some tests did not pass: 3 passed, 1 failed, 1 errored, 1 broken.
1715+
Test Summary: \| Pass Fail Error Broken Total Time
1716+
outer \| 3 1 1 1 6 \s*\d*.\ds
1717+
a \| 1 1 \s*\d*.\ds
1718+
custom \| 1 1 1 1 4 \s*\?s
1719+
no-record \| x x x x \? \s*\?s
1720+
b \| 1 1 \s*\d*.\ds
1721+
RNG of the outermost testset: .*
17221722
"""
17231723

17241724
cmd = `$(Base.julia_cmd()) --startup-file=no --color=no $f`
@@ -1753,3 +1753,20 @@ module M54082 end
17531753
@test only(result) isa Test.Fail
17541754
end
17551755
end
1756+
1757+
@testset "Set RNG of testset" begin
1758+
rng1 = Xoshiro(0x2e026445595ed28e, 0x07bb81ac4c54926d, 0x83d7d70843e8bad6, 0xdbef927d150af80b, 0xdbf91ddf2534f850)
1759+
rng2 = Xoshiro(0xc380f460355639ee, 0xb39bc754b7d63bbf, 0x1551dbcfb5ed5668, 0x71ab5a18fec21a25, 0x649d0c1be1ca5436)
1760+
rng3 = Xoshiro(0xee97f5b53f7cdc49, 0x480ac387b0527d3d, 0x614b416502a9e0f5, 0x5250cb36e4a4ceb1, 0xed6615c59e475fa0)
1761+
1762+
@testset rng=rng1 begin
1763+
@test rand() == rand(rng1)
1764+
end
1765+
1766+
@testset rng=rng2 "Outer" begin
1767+
@test rand() == rand(rng2)
1768+
@testset rng=rng3 "Inner: $(i)" for i in 1:10
1769+
@test rand() == rand(rng3)
1770+
end
1771+
end
1772+
end

0 commit comments

Comments
 (0)