diff --git a/src/Primitives.Async.Shared/AsyncContext.cs b/src/Primitives.Async.Shared/AsyncContext.cs
index d66f2f0..409f06b 100644
--- a/src/Primitives.Async.Shared/AsyncContext.cs
+++ b/src/Primitives.Async.Shared/AsyncContext.cs
@@ -95,12 +95,15 @@ public static AsyncContext From(ISequencer scheduler)
}
/// Gets the current asynchronous context associated with the calling thread.
- /// Use this method to capture the context for scheduling asynchronous operations that should
- /// continue on the same logical thread or synchronization context. This is commonly used to ensure code executes on
- /// the appropriate context, such as a UI thread in desktop applications.
+ ///
+ /// Use this method to capture the context for scheduling asynchronous operations that should continue on the same
+ /// logical thread or synchronization context. Coverage excludes this method because both inputs are ambient thread
+ /// state and cannot be changed safely by parallel tests.
+ ///
/// An representing the current asynchronous context. If a is present, it is used; otherwise, the current is
/// used.
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public static AsyncContext GetCurrent()
{
var currentSc = SynchronizationContext.Current;
diff --git a/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs b/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs
index 640cf5e..7ced352 100644
--- a/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs
+++ b/src/ReactiveUI.Primitives/Concurrency/SynchronizationContextSequencer.cs
@@ -16,10 +16,14 @@ public SynchronizationContextSequencer(SynchronizationContext context) =>
Context = context ?? throw new ArgumentNullException(nameof(context));
/// Gets a sequencer for the current synchronization context.
+ /// Coverage excludes the getter because the ambient context cannot be changed safely by parallel tests.
/// There is no current synchronization context.
- public static SynchronizationContextSequencer Current =>
- new(SynchronizationContext.Current
+ public static SynchronizationContextSequencer Current
+ {
+ [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ get => new(SynchronizationContext.Current
?? throw new InvalidOperationException("There is no current synchronization context."));
+ }
/// Gets the synchronization context used to schedule work.
public SynchronizationContext Context { get; }
diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs
index eb107aa..9062f15 100644
--- a/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs
+++ b/src/tests/ReactiveUI.Primitives.Async.Tests/AsyncRenameCoverageTests.cs
@@ -54,24 +54,11 @@ await Assert.That(
false)).IsFalse();
}
- /// Verifies current-context capture and explicit awaiter scheduling branches.
+ /// Verifies explicit awaiter scheduling branches.
/// A task representing the asynchronous test.
[Test]
- public async Task AsyncContextCurrentAndSwitcherBranchesCoverCustomSchedulersAndCancellation()
+ public async Task AsyncContextSwitcherBranchesCoverCustomSchedulersAndCancellation()
{
- var previous = SynchronizationContext.Current;
- SynchronizationContext currentContext = new();
- try
- {
- SynchronizationContext.SetSynchronizationContext(currentContext);
- var captured = AsyncContext.GetCurrent();
- await Assert.That(captured.SynchronizationContext).IsSameReferenceAs(currentContext);
- }
- finally
- {
- SynchronizationContext.SetSynchronizationContext(previous);
- }
-
var cancellationCallbacks = 0;
using CancellationTokenSource cancellation = new();
await cancellation.CancelAsync().ConfigureAwait(false);
diff --git a/src/tests/ReactiveUI.Primitives.Async.Tests/SyncLatestOperatorTests.cs b/src/tests/ReactiveUI.Primitives.Async.Tests/SyncLatestOperatorTests.cs
index 2f1a46f..87f6b7b 100644
--- a/src/tests/ReactiveUI.Primitives.Async.Tests/SyncLatestOperatorTests.cs
+++ b/src/tests/ReactiveUI.Primitives.Async.Tests/SyncLatestOperatorTests.cs
@@ -97,9 +97,11 @@ public partial class SyncLatestOperatorTests
[Test]
public async Task WhenSyncLatestOverEnumerable_ThenEmitsSnapshotOfLatestValues()
{
+ _ = Assert.Throws(static () =>
+ _ = new SyncLatestEnumerableSignal(null!, static values => values[0]));
var first = Signal.Create();
var second = Signal.Create();
- IObservableAsync[] sources = [first.Values, second.Values];
+ List> sources = [first.Values, second.Values];
List snapshots = [];
await using var sub = await sources.SyncLatest().SubscribeAsync(
diff --git a/src/tests/ReactiveUI.Primitives.Tests/SequencerTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SequencerTests.cs
index 6c9a70c..9fa3477 100644
--- a/src/tests/ReactiveUI.Primitives.Tests/SequencerTests.cs
+++ b/src/tests/ReactiveUI.Primitives.Tests/SequencerTests.cs
@@ -559,18 +559,6 @@ private static async Task AssertSynchronizationContextSequencerValidatesAndRunsS
{
ImmediateSynchronizationContext synchronizationContext = new();
_ = Assert.Throws(CreateSynchronizationContextSequencerWithoutContext);
- var previousContext = SynchronizationContext.Current;
- try
- {
- SynchronizationContext.SetSynchronizationContext(synchronizationContext);
- await Assert.That(SynchronizationContextSequencer.Current.Context)
- .IsSameReferenceAs(synchronizationContext);
- }
- finally
- {
- SynchronizationContext.SetSynchronizationContext(previousContext);
- }
-
SynchronizationContextSequencer synchronizationSequencer = new(synchronizationContext);
await Assert.That(synchronizationSequencer.Now > DateTimeOffset.MinValue).IsTrue();
_ = Assert.Throws(() => synchronizationSequencer.Schedule(One, null!));
diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalAliasCoverageTests.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalAliasCoverageTests.cs
index 02df22a..5ba040c 100644
--- a/src/tests/ReactiveUI.Primitives.Tests/SignalAliasCoverageTests.cs
+++ b/src/tests/ReactiveUI.Primitives.Tests/SignalAliasCoverageTests.cs
@@ -120,6 +120,14 @@ public async Task ParityOperatorsCoverRangeAndAliasBranches()
_ = Signal.PairLatest(Signal.Range(One, Two), Signal.Range(Three, Two), static (left, right) => left + right)
.Subscribe(latestRanges.Add);
await Assert.That(latestRanges.SequenceEqual([Two + Three, Two + Four])).IsTrue();
+
+ List latestWithNonRangeRight = [];
+ _ = Signal.PairLatest(
+ Signal.Range(One, Two),
+ Signal.FromEnumerable([Three, Four]),
+ static (left, right) => left + right)
+ .Subscribe(latestWithNonRangeRight.Add);
+ await Assert.That(latestWithNonRangeRight.SequenceEqual([Two + Three, Two + Four])).IsTrue();
}
/// Verifies direct from-async subscriptions cover constructor and synchronous completion paths.
diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalFactoriesTests.Composition.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalFactoriesTests.Composition.cs
index 57a60a8..0165777 100644
--- a/src/tests/ReactiveUI.Primitives.Tests/SignalFactoriesTests.Composition.cs
+++ b/src/tests/ReactiveUI.Primitives.Tests/SignalFactoriesTests.Composition.cs
@@ -75,12 +75,6 @@ public partial class SignalFactoriesTests
/// The divisor that selects the even values of a sequence.
private const int EvenDivisor = 2;
- /// Delay used by the async enumerable cancellation test.
- private const int AsyncEnumeratorDelayMilliseconds = 5000;
-
- /// Timeout used while waiting for async enumerable disposal.
- private const int AsyncEnumeratorDisposeTimeoutSeconds = 5;
-
/// Virtual clock due time for one-shot timers.
private const int AfterTicks = 5;
@@ -291,12 +285,15 @@ public async Task RetryResubscribesUntilSuccess()
}
/// Verifies async enumerable subscriptions cancel and dispose the enumerator.
+ /// The test cancellation token.
/// A task that completes when the asynchronous assertions have run.
[Test]
- public async Task AsyncEnumerableFactoryCancelsEnumeratorOnDispose()
+ [Timeout(30_000)]
+ public async Task AsyncEnumerableFactoryCancelsEnumeratorOnDispose(CancellationToken testToken)
{
var disposed = false;
List values = [];
+ TaskCompletionSource firstValueObserved = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource disposedSignal = new(TaskCreationOptions.RunContinuationsAsynchronously);
async IAsyncEnumerable Values([EnumeratorCancellation] CancellationToken token = default)
@@ -304,7 +301,7 @@ async IAsyncEnumerable Values([EnumeratorCancellation] CancellationToken to
try
{
yield return FirstValue;
- await Task.Delay(AsyncEnumeratorDelayMilliseconds, token);
+ await Task.Delay(Timeout.InfiniteTimeSpan, token);
yield return SecondValue;
}
finally
@@ -314,14 +311,17 @@ async IAsyncEnumerable Values([EnumeratorCancellation] CancellationToken to
}
}
- var subscription = Signal.FromAsyncEnumerable(Values()).Subscribe(
- values.Add,
+ var subscription = Signal.FromAsyncEnumerable(Values(testToken), testToken).Subscribe(
+ value =>
+ {
+ values.Add(value);
+ _ = firstValueObserved.TrySetResult();
+ },
static _ => { },
static () => { });
- await Task.Yield();
+ await firstValueObserved.Task.WaitAsync(testToken).ConfigureAwait(false);
subscription.Dispose();
- await disposedSignal.Task.WaitAsync(TimeSpan.FromSeconds(AsyncEnumeratorDisposeTimeoutSeconds))
- .ConfigureAwait(false);
+ await disposedSignal.Task.WaitAsync(testToken).ConfigureAwait(false);
await Assert.That(values.SequenceEqual(AsyncEnumerableBeforeDisposeExpected)).IsTrue();
await Assert.That(disposed).IsTrue();
}
diff --git a/src/tests/ReactiveUI.Primitives.Tests/SignalFromTaskTest.cs b/src/tests/ReactiveUI.Primitives.Tests/SignalFromTaskTest.cs
index 3f7cdf6..e0e128c 100644
--- a/src/tests/ReactiveUI.Primitives.Tests/SignalFromTaskTest.cs
+++ b/src/tests/ReactiveUI.Primitives.Tests/SignalFromTaskTest.cs
@@ -35,12 +35,6 @@ public class SignalFromTaskTest
/// Delay used before checking that a task has started.
private const int InitialDelayMilliseconds = 500;
- /// Delay before token cancellation is requested.
- private const int TokenCancellationDelayMilliseconds = 1000;
-
- /// Delay used to simulate cancellation cleanup.
- private const int CleanupDelayMilliseconds = 5000;
-
///
/// Time spent performing synchronous cancellation cleanup work. Kept short so the
/// blocking does not occupy a thread-pool thread long
@@ -712,11 +706,15 @@ await Task.Delay(CommandDelayMilliseconds, cts.Token)
public async Task SignalFromTask_T_HandlesUserExceptions()
{
StatusTrail statusTrail = new();
+ TaskCompletionSource executionStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ TaskCompletionSource releaseExecution = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ TaskCompletionSource finallyCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
var position = 0;
var fixture = Signal.FromTask(async cts =>
{
RecordStatus(statusTrail, ref position, StartedCommand);
- await Task.Delay(CommandDelayMilliseconds, cts.Token)
+ _ = executionStarted.TrySetResult();
+ await releaseExecution.Task.WaitAsync(cts.Token)
.HandleCancellation(() => RecordCancellationCleanup(statusTrail, ref position)).ConfigureAwait(true);
if (!cts.IsCancellationRequested)
{
@@ -728,14 +726,17 @@ await Task.Delay(CommandDelayMilliseconds, cts.Token)
{
RecordStatus(statusTrail, ref position, ExceptionShouldBeHere);
return Signal.Fail(ex);
- }).OnCleanup(() => RecordStatus(statusTrail, ref position, ShouldAlwaysComeHere));
+ }).OnCleanup(() =>
+ {
+ RecordStatus(statusTrail, ref position, ShouldAlwaysComeHere);
+ _ = finallyCompleted.TrySetResult();
+ });
var result = false;
- var subscription = fixture.Subscribe(_ => result = true);
- await Task.Delay(InitialDelayMilliseconds).ConfigureAwait(true);
+ using var subscription = fixture.Subscribe(_ => result = true);
+ await executionStarted.Task.WaitAsync(PollTimeout).ConfigureAwait(false);
await Assert.That(StatusMessages(statusTrail)).Contains(StartedCommand);
- await Task.Delay(CommandDelayMilliseconds).ConfigureAwait(true);
- subscription.Dispose();
- await Task.Delay(CancellationWaitDelayMilliseconds).ConfigureAwait(false);
+ _ = releaseExecution.TrySetResult();
+ await finallyCompleted.Task.WaitAsync(PollTimeout).ConfigureAwait(false);
await Assert.That(StatusMessages(statusTrail)).DoesNotContain(StartingCancellingCommand);
await Assert.That(StatusMessages(statusTrail)).Contains(ShouldAlwaysComeHere);
await Assert.That(StatusMessages(statusTrail)).DoesNotContain(FinishedCancellingCommand);
@@ -786,20 +787,20 @@ await Task.Delay(CommandDelayMilliseconds, cts.Token)
public async Task SignalFromTask_T_HandlesTokenCancellation()
{
StatusTrail statusTrail = new();
+ TaskCompletionSource cancellationReady =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource cleanupCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource finallyCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
var position = 0;
var fixture = Signal.FromTask(async cts =>
{
RecordStatus(statusTrail, ref position, StartedCommand);
- await Task.Delay(TokenCancellationDelayMilliseconds, cts.Token).HandleCancellation().ConfigureAwait(true);
- var cancellationTask = CancelAfterDelayAsync(cts);
- await Task.Delay(CleanupDelayMilliseconds, cts.Token).HandleCancellation(() =>
+ _ = cancellationReady.TrySetResult(cts);
+ await Task.Delay(Timeout.InfiniteTimeSpan, cts.Token).HandleCancellation(() =>
{
RecordCancellationCleanup(statusTrail, ref position);
_ = cleanupCompleted.TrySetResult();
}).ConfigureAwait(true);
- await cancellationTask.ConfigureAwait(false);
if (!cts.IsCancellationRequested)
{
RecordStatus(statusTrail, ref position, FinishedCommandNormally);
@@ -817,8 +818,9 @@ await Task.Delay(CleanupDelayMilliseconds, cts.Token).HandleCancellation(() =>
});
var result = false;
using var subscription = fixture.Subscribe(_ => result = true);
- await Task.Delay(InitialDelayMilliseconds).ConfigureAwait(true);
+ var cancellationSource = await cancellationReady.Task.WaitAsync(PollTimeout).ConfigureAwait(false);
await Assert.That(StatusMessages(statusTrail)).Contains(StartedCommand);
+ await cancellationSource.CancelAsync().ConfigureAwait(false);
await WaitForCancellationCallbacks(cleanupCompleted.Task, finallyCompleted.Task).ConfigureAwait(false);
await Assert.That(StatusMessages(statusTrail)).Contains(StartingCancellingCommand);
await Assert.That(StatusMessages(statusTrail)).Contains(ShouldAlwaysComeHere);
@@ -1063,15 +1065,6 @@ private static void RecordCancellationCleanup(StatusTrail statusTrail, ref int p
RecordStatus(statusTrail, ref position, FinishedCancellingCommand);
}
- /// Cancels the source after the token cancellation delay.
- /// The cancellation source.
- /// A representing the asynchronous operation.
- private static async Task CancelAfterDelayAsync(CancellationTokenSource cts)
- {
- await Task.Delay(TokenCancellationDelayMilliseconds, cts.Token).ConfigureAwait(false);
- await cts.CancelAsync().ConfigureAwait(false);
- }
-
/// Observer that counts the notifications it received.
private class RecordingObserver : IObserver
{