Skip to content

chore: de-flake the parallel test suite + clear the CRAN hidden-files NOTE - #152

Open
aviezerl wants to merge 2 commits into
masterfrom
chore/test-harness-robustness
Open

chore: de-flake the parallel test suite + clear the CRAN hidden-files NOTE#152
aviezerl wants to merge 2 commits into
masterfrom
chore/test-harness-robustness

Conversation

@aviezerl

@aviezerl aviezerl commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Test-harness only — no changes to package code. All of this came out of diagnosing #151, where the local suite gave 7, then 0, then 2 failures on effectively identical code.

The bug

Every one of those failures was the same thing wearing a different mask: .misha$GROOT left pointing at a directory that had been deleted. Under TESTTHAT_PARALLEL testthat runs several files per worker process, so file N can strand file N+1. The symptoms looked unrelated to each other and unrelated to their own test files:

  • Interval test.fixedbin does not exist
  • Chromosome chr1 does not exist ... Known chromosomes: chrA
  • Cannot delete track from read-only database (a removed directory isn't writable, so file.access reports it read-only)

Two halves:

  1. create_isolated_test_db()'s teardown called rm("GROOT") when the previous root had already been removed — which is exactly what happens when the preceding file in the worker was itself isolated. The worker was left with no database at all.
  2. Ten files build their own temp DB and re-root into it, then let withr delete the directory while still rooted there.

27 files re-root without restoring, but only the 10 that delete the directory mid-run can strand a later file. ensure_valid_groot() / restore_groot_on_exit() in helper-test_db.R handle both halves; for helper-liftover.R the restore goes into its existing teardown_env() defer, which covers every file that uses it.

Also

  • Fail loudly. create_isolated_test_db() ignored the exit status of its cp -r / ln -s calls, so a full tempdir() produced a half-built DB that failed much later somewhere else. It now errors at the point of failure and names tempdir(). This earned its keep immediately: it fired zero times during the FAIL 7 run, which ruled it out as the cause in one grep.
  • CRAN NOTE. .Rbuildignore the two hidden files under tests/testthat/fixtures/tiny-hub-groot. Their only consumer is skip_on_cran()-gated. Verified against a real tarball: no hidden files at all, fixture content intact.
  • .Rbuildignore ^\.worktrees$. Local git worktrees live inside the repo, so R CMD build was sweeping all 15 into the tarball — 3.1 GB instead of 1.8 MB, which made local R CMD build / check unusable. No effect on CI, which builds from a fresh clone.
  • CONTRIBUTING.md documents the TMPDIR requirement for parallel runs, and the tell that failures moving between files across runs mean disk exhaustion rather than flaky tests.

Verification

alutil::tst(parallel = TRUE) three consecutive times: FAIL 0 | WARN 92 | SKIP 32 | PASS 19271, identical each run. The pass count matches the pre-change green run, so nothing is being skipped.

Not claimed: that this makes the suite provably deterministic. Three clean runs is evidence, not proof, and the remaining 17 re-rooting files are fine only because they don't delete their DB mid-run.

Three test-harness fixes, all found while diagnosing PR #151.

1. .Rbuildignore the two hidden files under
   tests/testthat/fixtures/tiny-hub-groot (.db.cache, .ro_attributes). They
   produced a "checking for hidden files and directories" NOTE on every CRAN
   check. Their only consumer is skip_on_cran()-gated, and .Rbuildignore
   affects the built tarball only, not devtools::test.

2. create_isolated_test_db() now checks the exit status of its cp -r / ln -s
   calls and of the chrom_sizes.txt copy. Previously a failure (a full
   tempdir() being the usual cause) left a half-built DB and surfaced much
   later as e.g. "Interval test.fixedbin does not exist" in an unrelated file,
   which is exactly how it wasted time on #151.

3. Its teardown no longer unsets GROOT when the previous root has already been
   removed. That happens whenever the preceding file in a parallel worker was
   itself isolated: the worker was left with no database, and the next file
   relying on the ambient root failed with "Database directory does not exist"
   or "Chromosome chr1 does not exist ... Known chromosomes: chrA". It now
   falls back to the shared test db, which covers the ~22 files that read main
   db fixtures without isolating.

Also adds create_isolated_test_db() to test-ggenome.R and
test-gintervals-from-strings.R (the two that failed this way), and documents
the TMPDIR requirement for parallel runs in CONTRIBUTING.md.

This does not make the suite fully deterministic. Files that manage their own
temp DBs can still leave GROOT dangling after withr deletes the directory -
test-gtrack-create-meta-indexed.R's setup_db() does exactly that and still
fails intermittently. That is a separate cleanup.

Claude-Session: https://claude.ai/code/session_01GCdUQC4k8tf93iHnZHSTVS
Follow-up to the previous commit, which fixed create_isolated_test_db()'s
teardown but left the other half of the problem: files that build their own
temporary database and re-root into it. When withr removes that directory,
.misha$GROOT still points at it, and under TESTTHAT_PARALLEL the next file in
the same worker inherits the dangling root. It then fails on something
unrelated to its own subject - "Database directory does not exist",
"Chromosome chr1 does not exist ... Known chromosomes: chrA", or "Cannot
delete track from read-only database" (a removed directory is not writable,
so file.access reports it as read-only).

27 files re-root without restoring, but only the 10 that delete the directory
mid-run can strand a later file. Adds ensure_valid_groot() /
restore_groot_on_exit() to helper-test_db.R and calls them there. For
helper-liftover.R the restore goes in its existing teardown_env() defer, which
covers every file that uses it.

Also .Rbuildignore ^\.worktrees$. Local git worktrees live inside the repo, so
R CMD build was sweeping all 15 of them into the tarball: 3.1 GB instead of
1.8 MB, which made local R CMD build/check unusable. Verified the built
tarball now contains no hidden files at all and no worktrees.

Suite run three consecutive times: FAIL 0 | PASS 19271 each time. Before these
two commits the same code gave 7, 0 and 2 failures on successive runs.

Claude-Session: https://claude.ai/code/session_01GCdUQC4k8tf93iHnZHSTVS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant