fix(test): stop parallel tests racing the shared Bogus Noda cache - #579
Conversation
NaiveFixture held one static Faker<BookRoom>, and f.Noda() caches its dataset in that Faker's Bogus.Premium context — a plain Dictionary. TUnit runs tests in parallel, so several could reach the cache on its first, still-empty use and corrupt it, failing the suite with "Operations that change non-concurrent collections must have exclusive access" from inside Bogus. Builds the Faker per call instead, so each generation gets its own context. That is already how DomainFixture is written in the Persistence, Redis, KurrentDB and MongoDB suites, which is why none of them flake. Measured with a standalone harness of 200 cold rounds, 32 threads each: the shared-instance shape failed 70 rounds, the per-call shape none. Fixes #566 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR Summary by QodoFix parallel test flake from shared Bogus Noda cache in NaiveFixture
AI Description
Diagram
High-Level Assessment
Files changed (1)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships |
Test Results 44 files + 21 44 suites +21 12m 31s ⏱️ - 6m 3s Results for commit 649dd95. ± Comparison against base commit 227c010. This pull request removes 26 and adds 9 tests. Note that renamed tests count towards both. |
Fixes #566.
What was wrong
NaiveFixtureheld a singlestatic readonly Faker<Commands.BookRoom>.f.Noda()resolves its dataset throughBogus.Premium.ContextHelper, which caches into((IHasContext)faker).Context— a plainDictionary<string, object>that belongs to that oneFakerinstance. TUnit runs tests in parallel, so several could hit the cache while it was still empty and corrupt it:The fix
Build the
Fakerper call (=>) instead of once (=), so each generation gets its own context dictionary and there is nothing to share.This is already how
DomainFixtureis written inEventuous.Tests.Persistence.Base,Eventuous.Tests.Redis,Eventuous.Tests.KurrentDBandEventuous.Tests.Projections.MongoDB— all four use an expression-bodied property.NaiveFixturewas the only fixture with a shared instance, which is why it was the only one flaking. The other suites needed no change.Verification
Decompiled Bogus 35.6.5 to confirm the cache is per-
Faker:ContextHelper.GetOrSetwrites to((IHasContext)f).Context, an instance auto-property initialised tonew Dictionary<string, object>(), andFaker<T>.FakerHubis an instance field. ConcurrentFakerconstruction is safe —Bogus.Database.Datais aLazy<ConcurrentDictionary<..>>withExecutionAndPublication.A standalone harness comparing the two shapes over 200 cold rounds of 32 threads each:
static readonly Faker<T> Faker = new(...)static Faker<T> Faker => new(...)Eventuous.Testsonnet10.0then ran 12 consecutive times, 29 tests each, all green. The reported rate was roughly 1 in 10 runs, so the repeat runs are corroboration; the harness numbers and the removal of the shared instance are the actual evidence.Not changed
src/Experimental/src/ElasticPlayground/Generator.cshas the same shared-static-Fakershape, but it is a manual playground whose scenarios run sequentially on one thread, so it is not at risk. Left alone to keep this diff to the reported bug.🤖 Generated with Claude Code