test: withr-free local seed and RNG-state leak fixes#2714
Open
schochastics wants to merge 5 commits into
Open
Conversation
The test_that override in setup.R fails any test that changes the global
.Random.seed (gated behind IGRAPH_CHECK_RNG_STATE). Several things had to
change for the suite to pass under it:
* Move helper functions defined inside test files into a shared
helper-test-functions.R. The override re-scopes test code, so file-local
helpers were no longer reachable from the test body.
* Add igraph_local_seed() (helper.R): a withr::local_seed-free local seed that
snapshots and restores RNGkind + .Random.seed itself, restoring the kind
first and the seed last so the state round-trips exactly. It takes an
optional rng_version, replacing the leaky local_rng_version() +
withr::local_seed() pairing (whose two independent deferred restores ran in
the wrong order).
* setup.R: set a baseline seed so .Random.seed always exists -- otherwise the
first RNG-touching test creates it from nothing (many igraph C functions init
the RNG without drawing, e.g. voronoi_cells()), which the check flagged.
Also forward the test block via substitute()/eval() instead of
rlang::inject(testthat::test_that(name, !!code)); inject() mangled test
bodies containing !!/!!!/{{ (e.g. set_vertex_attrs(g, !!!attr_list)).
* Add igraph_local_seed() to the tests that genuinely advance the RNG
(unseeded sample_* fixtures and ARPACK eigensolvers), and convert the
local_rng_version() + withr::local_seed() pairs to
igraph_local_seed(rng_version = "3.5.0").
Full suite passes the check: 0 seed leaks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate all 875 withr::local_seed() call sites in the test suite to
igraph_local_seed(), completing the move off withr for RNG-seed handling.
All calls passed only a seed, so this is a direct swap; igraph_local_seed()
restores RNGkind + .Random.seed itself (a superset of withr::local_seed's
behaviour).
Also:
* restore_rng_state() now suppresses warnings around the RNGkind restore, so
putting back an older sampler (the "Rounding" sample.kind from
RNGversion("3.5.0")) no longer emits R's "non-uniform 'Rounding' sampler
used" note. This surfaced once the inner withr::local_seed calls nested under
igraph_local_seed(rng_version = "3.5.0") were migrated; withr had suppressed
it.
* tools/AGENTS.md: update the test-writing guidance to use igraph_local_seed so
newly added test-aaa-auto.R tests follow the convention.
Full suite passes the seed-leak check with no warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add igraph_with_seed(), a block-form companion to igraph_local_seed() that mirrors withr::with_seed(): set the seed, evaluate the block, restore the prior global RNG state. It is needed where a single test has several independently seeded blocks with code between them (e.g. test-print-classic.R), which a frame-scoped igraph_local_seed() can't express. Migrate the 9 withr::with_seed() call sites (test-print-classic.R, test-layout.R, test-iterators.R) to igraph_with_seed(), completing the removal of withr from the suite's RNG-seed handling. These never leaked (with_seed is self-contained); this is consistency cleanup. Snapshots unchanged; suite passes the seed-leak check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dead code: every local_rng_version("3.5.0") call was folded into
igraph_local_seed(seed, rng_version = "3.5.0") earlier in this branch, leaving
no call sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
The test suite has an opt-in check (gated behind
IGRAPH_CHECK_RNG_STATE=trueintests/testthat/setup.R) that fails any test which leaves the global.Random.seedchanged. This PR makes the whole suite pass that check and movesthe suite's RNG-seed handling off
withronto in-house helpers.Running the suite with
IGRAPH_CHECK_RNG_STATE=true: 0 seed leaks, 0failures, 0 warnings (9739 passing).
New helpers (
tests/testthat/helper.R)igraph_local_seed()— drop-in forwithr::local_seed(). Sets the seed forthe current scope and restores the previous global RNG state when that scope
exits.
RNGkind+.Random.seeditself(uses
withr::deferonly as the deferral primitive), so it doesn't inheritwithr::local_seed's past seed-handling bugs..Random.seedlast — changing the kinddiscards the seed, so the seed write must come last for an exact round-trip.
rng_version(e.g."3.5.0"), folding in what used to be a separatelocal_rng_version()call. Pairing two independent deferred restores (kind +seed) ran them in the wrong order and leaked; a single ordered restore fixes it.
igraph_with_seed()— block form mirroringwithr::with_seed(): set theseed, evaluate the block, restore. Needed where a single test has several
independently seeded blocks with code between them (e.g.
test-print-classic.R),which a frame-scoped
igraph_local_seed()can't express.setup.R
set.seed(42)) so.Random.seedalways exists beforetests run. Otherwise the first RNG-touching test creates it from nothing
(many igraph C functions initialise the RNG without drawing from it, e.g.
voronoi_cells()), which the check flagged — and restoring that absent statejust shoved the "first creation" onto the next test (whack-a-mole).
substitute()+eval(as.call(...))instead ofrlang::inject(testthat::test_that(name, !!code)).inject()eagerlyprocessed any injection operator in the test body (
!!,!!!,{{),e.g.
set_vertex_attrs(g, !!!attr_list), evaluating it in the wrong frame anderroring with
object 'attr_list' not found.IGRAPH_CHECK_RNG_STATE=true(opt-in for CI).Test files
helper-test-functions.R(the override re-scopes test code, so file-localhelpers were no longer reachable from the test body).
igraph_local_seed()to tests that genuinely advance the RNG (unseededsample_*fixtures and ARPACK eigensolvers).withr::local_seed()call sites withigraph_local_seed()(including the 732 intest-aaa-auto.R).withr::with_seed()call sites withigraph_with_seed()(
test-print-classic.R,test-layout.R,test-iterators.R). These neverleaked — consistency cleanup; snapshots unchanged.
local_rng_version("3.5.0")+ seed pairing withigraph_local_seed(seed, rng_version = "3.5.0"), then removed the now-unusedlocal_rng_version()helper.restore_rng_state()suppresses warnings around theRNGkindrestore soputting back the older
"Rounding"sampler (RNGversion("3.5.0")) doesn'temit R's "non-uniform 'Rounding' sampler used" note.
tools/AGENTS.md
Updated the test-writing guidance to use
igraph_local_seed, so newly addedtest-aaa-auto.Rtests follow the convention.Notes
igraph_local_seed()/igraph_with_seed()live inhelper.R(notsetup.R)so helper functions like
make_scan_graphs()can see them.withrfor any RNG-seed handling.🤖 Generated with Claude Code