Skip to content

test: expand coverage for fused async helpers, subject base, partition, and sync operator branches#167

Open
glennawatson wants to merge 15 commits into
mainfrom
tests/expand-operator-coverage-round-3
Open

test: expand coverage for fused async helpers, subject base, partition, and sync operator branches#167
glennawatson wants to merge 15 commits into
mainfrom
tests/expand-operator-coverage-round-3

Conversation

@glennawatson
Copy link
Copy Markdown
Contributor

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

  • ParityHelpers.OperatorFusions (56 uncovered → reduced): async ScanWithInitial, ThrottleDistinct distinct semantics, DebounceUntil immediate-bypass, ForEach typed fast paths (array / IReadOnlyList / general IEnumerable)
  • Concurrent async-subject base helpers (15 uncovered): empty / single / sync-fast-path / slow-path Task.WhenAll branches for OnNext / OnErrorResume / OnCompleted fan-out
  • ParityHelpers.FilterFusions (9 uncovered): SkipWhileNull, WhereIsNotNull, LatestOrDefault, WaitUntil, AsSignal, Not, WhereTrue, WhereFalse plus error forwarding
  • FirstMatchFromCandidatesObservable (7 uncovered): empty list, async-projection match / no-match / error / dispose-during-walk paths
  • UsingActionObservable (residual 12): secondary-dispose-failure swallow branch, scheduler-action-throws forwarding
  • PartitionObservable (residual 11): three-observer same-side broadcast, mid-array existing.Length > 2 shrink branch, idempotent subscription dispose, post-drop completion safety
  • AsyncGate (5 uncovered): uncontended fast path, same-thread reentry, contended slow path via semaphore, idempotent dispose
  • ShuffleObservable (5 uncovered): multiset preservation, null array passthrough, error / completion forwarding
  • FilterRegexObservable (4 uncovered): pattern + precompiled regex overloads, null-input skip, regex-timeout error forwarding
  • TrySelectObservable (4 uncovered): null-projection drop, selector-throws forwarding, completion forwarding

Notes

  • Regex tests use the [GeneratedRegex] source generator (not new Regex(...)) — SYSLIB1045 is treated as an error and the project intentionally requires the compile-time generator.
  • All analyzer rules satisfied (StyleCop, Roslynator, Sonar S109/S122/S1192/S3877/SA1107/SA1202, CA1822/CA1859/CA2016, IDE0028/IDE0090/IDE0300/IDE0306, RCS1005/RCS1208) without project-wide suppressions.

Test plan

  • dotnet build ReactiveUI.Extensions.slnx -c Release -warnaserror — clean
  • dotnet test --solution ReactiveUI.Extensions.slnx -c Release — 4758 tests, 0 failed across net8.0 / net9.0 / net10.0

- 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
@glennawatson glennawatson changed the title test: round-3 coverage expansion across 10 more classes test: expand coverage for fused async helpers, subject base, partition, and sync operator branches May 19, 2026
…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
Copy link
Copy Markdown

codecov Bot commented May 19, 2026

Codecov Report

❌ Patch coverage is 91.42857% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.05%. Comparing base (0fde9d5) to head (9355e4c).

Files with missing lines Patch % Lines
...s/Async/Operators/ParityHelpers.OperatorFusions.cs 88.46% 1 Missing and 2 partials ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…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.
[Test]
public async Task WhenAllSourcesComplete_ThenForwardsCompletion()
{
var a = new Subject<bool>();
public async Task WhenAllSourcesComplete_ThenForwardsCompletion()
{
var a = new Subject<bool>();
var b = new Subject<bool>();
…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.
@sonarqubecloud
Copy link
Copy Markdown

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