Improve StreamingTextNode thread-safety test coverage#226
Merged
Conversation
Follow-up to #225. A self-review surfaced two test-quality issues in the new race regression suite: 1. AnimationInvalidation_ConcurrentWithReplace_DoesNotThrow ordered Interlocked.Exchange AFTER node.Replace, so the ticker mostly read a reference to an already-disposed segment rather than racing a live one. The test mostly validated "TriggerInvalidation tolerates disposed segment" rather than the advertised live-vs-Replace race. Restructure: a permanently-tracked spinner is the ticker's target; Replace operates on a SEPARATE tracked element. The ticker is always firing on a live subscription, so OnAnimationInvalidated competes with Replace for _contentLock — which is the race the test claims to cover. 2. AnimationInvalidation_AfterDispose_DoesNotThrow passed for the wrong reason: node.Dispose() disposes the R3 subscription before the late TriggerInvalidation runs, so OnAnimationInvalidated is never invoked and the _disposed flag's belt-and-suspenders role is not exercised. Keep the original test (rebranded — it does validate subscription teardown, a legitimate concern) and add a new reflection-based test that invokes OnAnimationInvalidated directly after Dispose. Verified the new test FAILS with ObjectDisposedException from R3.Subject.OnNext when the `_disposed` guard is removed, and PASSES when the guard is present — so it actually catches the regression.
Aaronontheweb
added a commit
that referenced
this pull request
May 24, 2026
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.
Summary
Follow-up to #225. A self-review of the new race regression suite surfaced two coverage holes — both tests passed, but for the wrong reasons.
1. Replace stress test exercised the wrong race
AnimationInvalidation_ConcurrentWithReplace_DoesNotThroworderedInterlocked.Exchangeafternode.Replace(id, next). By the time Exchange ran, Replace had already disposed the old segment, so the ticker'sVolatile.Read(ref current)mostly returned a reference to an already-disposed segment. The test was effectively validating "TriggerInvalidation tolerates disposed segment" rather than the advertised live-segment-vs-Replace race.Fix: Restructure with two tracked elements — a permanently-tracked spinner the ticker fires on (always live), and a separate tracked element that Replace swaps repeatedly. The ticker is always firing on a live subscription, so
OnAnimationInvalidatedcompetes withReplacefor_contentLock, which is the race the test claims to cover.2. AfterDispose test passed for the wrong reason
AnimationInvalidation_AfterDispose_DoesNotThrowcallednode.AppendTracked, thennode.Dispose()(which disposes the R3 subscription), thenspinner.TriggerInvalidation. Since the subscription was already torn down,OnAnimationInvalidatedwas never invoked — the_disposedflag's belt-and-suspenders role was not exercised. The test would have passed even if the flag check were absent.Fix: Keep the original test (renamed
AnimationInvalidation_AfterDispose_SubscriptionDisposalUnhooksCallback— it does validate that subscription teardown unhooks the observer, which is real coverage) and addOnAnimationInvalidated_AfterDispose_BailsOutViaDisposedFlag, which uses reflection to invokeOnAnimationInvalidateddirectly afterDispose(). This simulates an in-flight callback that already passed the Subject's observer-list dispatch and is about to enter the lock when Dispose completes — exactly the scenario the flag protects.Verification
Confirmed the new reflection-based test correctly catches the regression: temporarily removed the
if (_disposed) return;guard and ran the test:Restored the guard — test passes again. Production code is unchanged in this PR.
Test plan
dotnet buildsucceeds, 0 warnings_disposedguard makes the new test fail with the expectedObjectDisposedExceptionfrom R3