test: expand coverage for fused async helpers, subject base, partition, and sync operator branches#167
Open
glennawatson wants to merge 15 commits into
Open
test: expand coverage for fused async helpers, subject base, partition, and sync operator branches#167glennawatson wants to merge 15 commits into
glennawatson wants to merge 15 commits into
Conversation
- ParityHelpers.OperatorFusions: async ScanWithInitial, ThrottleDistinct distinct semantics, DebounceUntil immediate bypass, ForEach typed-fast-paths (array / IReadOnlyList / IEnumerable) - ParityHelpers.FilterFusions: SkipWhileNull, WhereIsNotNull, LatestOrDefault, WaitUntil, AsSignal, Not, WhereTrue, WhereFalse + error forwarding - Concurrent (async subject base helpers): empty / single / sync-fast-path / slow-path Task.WhenAll branches for OnNext / OnErrorResume / OnCompleted fan-out - AsyncGate: uncontended fast path, same-thread reentry, contended slow path via semaphore, idempotent dispose - UsingActionObservable: secondary-dispose-failure swallow branch, scheduler-action-throws forwarding - PartitionObservable: three-observer same-side broadcast, mid-array dispose (shrink>2 path), idempotent subscription dispose, post-drop completion safety - FirstMatchFromCandidates: empty list, async-projection match / no-match / error / dispose-during-walk paths - ShuffleObservable: multiset preservation, null array passthrough, error / completion forwarding - FilterRegexObservable: pattern + precompiled regex overloads, null-input skip, regex-timeout error forwarding (uses [GeneratedRegex] source generator) - TrySelectObservable: null-projection drop, selector-throws forwarding, completion forwarding
| public async Task WhenAsyncProjectionMatches_ThenEmitsMatch() | ||
| { | ||
| string[] keys = ["miss", "hit"]; | ||
| var emissionGate = new Subject<string>(); |
| public async Task WhenAsyncProjectionNeverMatches_ThenFallback() | ||
| { | ||
| string[] keys = ["only"]; | ||
| var subject = new Subject<string>(); |
| public async Task WhenAsyncProjectionErrors_ThenSkipsToNextCandidate() | ||
| { | ||
| string[] keys = ["bad", "good"]; | ||
| var badSubject = new Subject<string>(); |
| { | ||
| string[] keys = ["bad", "good"]; | ||
| var badSubject = new Subject<string>(); | ||
| var goodSubject = new Subject<string>(); |
| public async Task WhenDisposedDuringAsyncWalk_ThenStops() | ||
| { | ||
| string[] keys = ["k1", "k2"]; | ||
| var firstSubject = new Subject<string>(); |
… operators - DetectStale: error / completion forwarding - BufferUntilIdle: error path flushes pending buffer before forwarding - DebounceImmediate: first-inline + debounce, flush-then-complete, flush-then-error - DebounceUntil: condition-true immediate bypass, condition-false debounce - Schedule (value overloads): relative-delay + absolute-due-time value scheduling - Schedule (source overloads): relative-delay + absolute-due-time per-emission scheduling - LatestOrDefault: seed + distinct + error forwarding - Pairwise: adjacent pairs, single-element completes empty, error forwarding - WaitUntil (sync): first-match emit-and-complete, error forwarding - SwitchIfEmpty: fallback on empty, passthrough on non-empty, error forwarding
2 tasks
…ry branches - WhereSelect: predicate filter + projection, predicate-throws, selector-throws, source completion forwarding - FromArray: inline pump, scheduler-dispatched pump, enumeration-throws forwarding - RetryWithDelay: retries up to the configured count with zero-delay scheduling - RetryForeverWithDelay: keeps retrying until source succeeds - ThrottleOnScheduler: window-driven latest-value emission, source-error forwarding - ThrottleDistinct (sync default scheduler overload): source-error forwarding - ThrottleDistinct (sync with-scheduler overload): upstream-distinct suppression - ToReadOnlyBehavior: paired observable/observer, replay initial + broadcast - SubscribeAndComplete: silently swallows a Unit-producing source's error
The 20ms 'waiter has not resumed' check was racy on macOS CI runners (job 76645935159) — it passed on Linux/Windows but the scheduler quantum on macOS is just short enough that the contender occasionally raced through the slow path before the probe ran. Drop the negative timing assertion. Coverage of the slow path is still exercised: the first acquisition is held synchronously, so the contender must go through the semaphore-park-and-retry path; the only thing that can let it resume is the first.Dispose() that follows. If the slow path were broken, secondAcquired.Task.WaitAsync would time out at 5s and the test fails.
OnCompleted is signalled before the resource is disposed on the scheduler thread, so the assertion could race the dispose. Spin briefly for the dispose count to land before asserting.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #167 +/- ##
==========================================
+ Coverage 92.61% 97.05% +4.43%
==========================================
Files 224 224
Lines 8503 8510 +7
Branches 930 931 +1
==========================================
+ Hits 7875 8259 +384
+ Misses 410 144 -266
+ Partials 218 107 -111 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…s sync operators and disposables Adds per-operator test classes mirroring the production class names for Not/ForEach/MinMax/LogErrors/RetryForever observables, the ActionDisposable/MutableDisposable/SwapDisposable/DelegateObserver internals, FirstAsTaskHelper null-source and double-settle paths, and the ObserveOnAsyncObservable forceYielding slow paths.
| [Test] | ||
| public async Task WhenSubjectErrorsThenLaterEvents_ThenLaterEventsIgnored() | ||
| { | ||
| var subject = new Subject<int>(); |
| [Test] | ||
| public async Task WhenForEachReceivesNullBatch_ThenIgnoresNullAndProcessesNext() | ||
| { | ||
| var subject = new Subject<IEnumerable<int>>(); |
…ternal-CT short-circuits, and ReactiveExtensions overloads Targets the gaps reported by the merged cobertura: PartitionCoordinator error broadcast and late-terminal cache, DropIfBusy sync-throw and OnErrorResume forwarding for the fused async Scan/Throttle/Debounce/ ForEach/DropIfBusy observers, the already-cancelled external token path in Merge/Zip/Switch, the multi-observer OnCompleted loop in CurrentValueSubject, and the overload pass-throughs / null guards in ScheduleSafe, OnErrorRetry<TSource,TException>, RetryWithBackoff, ReplayLastOnSubscribe, BufferUntilInactive, CatchReturn and CatchReturnUnit.
Adds a sync direct-source helper plus per-operator tests targeting the after-terminal-guard return branches in ThrottleObservable, ThrottleDistinctObservable, ThrottleFirstObservable, ThrottleUntilTrueObservable, ConflateObservable, ObserveOnIfObservable, SubscribeAsyncObservable, SelectAsyncConcurrentObservable, and SynchronizeAsyncObservable; the mid-array remove and idempotent dispose paths of SyncTimerObservable; the AwaitForwardAsync slow path of the fused async DropIfBusy; the async-accumulator OnErrorResume forwarding in ScanWithInitial; the already-cancelled subscribe-token short-circuit through ObserverAsync's LinkExternalCancellation; the linked-CTS slow path in BaseReplayLatestSubjectAsync; and the idempotent dispose of the partition branch subscription.
…async external-CT registrations, and after-terminal sinks Adds: SkipWhile/TakeWhile sync and async OnErrorResume forwarding plus the sync-completed async-predicate branches; FirstMatchFromCandidates sync-transform-throws continue path and post-match drop guard; PartitionObservable mid-array remove of false-side observers and the stale-subscription dispose no-op; Merge/Zip external-token registration slow path when the token is cancellable but not yet cancelled; and the after-terminal guards on SelectLatestAsyncObservable.
| public async Task WhenAsyncCandidateEmitsAfterMatch_ThenDroppedByDoneGuard() | ||
| { | ||
| string[] keys = ["hit"]; | ||
| var subject = new Subject<string>(); |
…rloads, throwing-downstream unhandled-exception paths, and remaining after-terminal sinks Adds: OnErrorResume forwarding for the seven async filter fusions (Pairwise, SkipWhileNull, LatestOrDefault, WaitUntil, AsSignal, Not, WhereTrue, WhereFalse); the cancellable-CT overloads of TakeUntil (other, task, predicate, async-predicate); the throwing-downstream catch blocks in ParityHelpers ThrottleDistinct and DebounceUntil and the synchronous/asynchronous throw paths in ObserverAsync's OnErrorResume/OnCompleted bookkeeping; the after-terminal guards on DebounceImmediateObservable and HeartbeatObservable; SwitchObservable's external-CT cancellation-after-subscribe registration; and double-settle guards on FirstAsTaskHelper's first-value observer via a non-cooperative test source.
…l, MinMax/BooleanReduce completion, Multicast custom-CT and double-dispose, ObserveOn slow-path; exclude TestResults from Sonar Adds: after-terminal sink guards on RunAllObservable, BooleanReduceObservable, MinMaxObservable, SampleLatestObservable (using SyncDirectSource to push events past terminal); the per-source OnCompleted forwarding in MinMax and BooleanReduce; the linked-CTS slow path and idempotent connection dispose on MulticastObservableAsync; the differing-SyncContext forceYielding:false slow path for ObserveOn error and completion forwarding. Also adds **/TestResults/** to sonar.exclusions so the test-run HTML reports stop appearing as indexed source on the catch-all module.
…ttach logic into testable internal methods Pulls three decision points out of fire-and-forget async loops: - ThrottleDistinctObserver.TryClaimEmission(value, id) — combined supersession and downstream-distinct check after the throttle window. - DebounceUntilObserver.IsCurrentEmission(id) — supersession check after the debounce window. - PartitionCoordinator.TryAttachSourceSubscription(subscription) — the both-branches-gone race finalizer after the source subscribe returns. Hot path callers now invoke these methods directly; the methods are internal so InternalsVisibleTo lets the test project unit-test them with synthesized observer state — no Task.Delay, no scheduler racing. Same IL on the hot path (private call → instance call on the same object, no extra allocation, no delegate dispatch).
…, async Scan paths, CombineLatest selector-throws, and ObserveOn slow-path via internal extraction After-terminal SyncDirectSource tests for WaitUntilObservable, ScanWithInitialObservable, SelectAsyncSequentialObservable. Async Scan: sync-completed accumulator fast path + OnErrorResume forwarding for both sync and async overloads. CombineLatestEnumerable: selector-throws → terminal-failure path. ObserveOnAsyncObservable: changed the three slow-path methods (SwitchThenForwardAsync / SwitchThenErrorAsync / SwitchThenCompletedAsync) from private to internal so they are directly unit-testable without racing the IsSameAsCurrentAsyncContext fast/slow decision. Same as the ParityHelpers refactor — keeping the decision logic on the same class with no allocation or delegate-dispatch overhead, just opening the testability surface to InternalsVisibleTo.
|
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
Third round of coverage tests, prioritised by uncovered-line count from the latest report. Targets 10 production classes; full suite stays green across net8/9/10 (4758 tests).
Classes covered
ScanWithInitial,ThrottleDistinctdistinct semantics,DebounceUntilimmediate-bypass,ForEachtyped fast paths (array /IReadOnlyList/ generalIEnumerable)Task.WhenAllbranches forOnNext/OnErrorResume/OnCompletedfan-outSkipWhileNull,WhereIsNotNull,LatestOrDefault,WaitUntil,AsSignal,Not,WhereTrue,WhereFalseplus error forwardingexisting.Length > 2shrink branch, idempotent subscription dispose, post-drop completion safetyNotes
[GeneratedRegex]source generator (notnew Regex(...)) — SYSLIB1045 is treated as an error and the project intentionally requires the compile-time generator.Test plan
dotnet build ReactiveUI.Extensions.slnx -c Release -warnaserror— cleandotnet test --solution ReactiveUI.Extensions.slnx -c Release— 4758 tests, 0 failed across net8.0 / net9.0 / net10.0