Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using Moq;
using Xunit;

Expand Down Expand Up @@ -1276,6 +1277,156 @@

#endregion

#region SyncContext Restoration Tests

[Fact]

Check failure on line 1282 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

CenterEdge.Async.UnitTests.AsyncHelperTests.RunSync_Task_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue

Assert.Equal() Failure: Values differ Expected: ReentrantSynchronizationContext { } Actual: null

Check failure on line 1282 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

CenterEdge.Async.UnitTests.AsyncHelperTests.RunSync_Task_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue

Assert.Equal() Failure: Values differ Expected: ReentrantSynchronizationContext { } Actual: null
public void RunSync_Task_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue()
{
// This test simulates the bug where a reentrant sync context (like WinForms)
// might process posted messages before RunSync returns, causing continuations
// to see the ExclusiveSynchronizationContext instead of the original context.

// Arrange
SynchronizationContext? capturedContext = null;

Check warning on line 1290 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 1290 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var reentrantSync = new ReentrantSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(reentrantSync);

try
{
// Act
AsyncHelper.RunSync(() =>
{
// Start a continuation that will be queued
#pragma warning disable CS4014
Task.Run(() =>
{
// This continuation will be posted back to the sync context
capturedContext = SynchronizationContext.Current;
});
#pragma warning restore CS4014

// Return a completed task so RunAlreadyComplete is called
return Task.CompletedTask;
});

// The reentrant context processes its queue before returning
reentrantSync.ProcessQueue();

// Assert
Assert.Equal(reentrantSync, capturedContext);
}
finally
{
SynchronizationContext.SetSynchronizationContext(null);
}
}

[Fact]

Check failure on line 1324 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

CenterEdge.Async.UnitTests.AsyncHelperTests.RunSync_ValueTask_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue

Assert.Equal() Failure: Values differ Expected: ReentrantSynchronizationContext { } Actual: null

Check failure on line 1324 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

CenterEdge.Async.UnitTests.AsyncHelperTests.RunSync_ValueTask_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue

Assert.Equal() Failure: Values differ Expected: ReentrantSynchronizationContext { } Actual: null
public void RunSync_ValueTask_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue()
{
// Arrange
SynchronizationContext? capturedContext = null;

Check warning on line 1328 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 1328 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var reentrantSync = new ReentrantSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(reentrantSync);

try
{
// Act
AsyncHelper.RunSync(() =>
{
#pragma warning disable CS4014
Task.Run(() =>
{
capturedContext = SynchronizationContext.Current;
});
#pragma warning restore CS4014

return new ValueTask();
});

reentrantSync.ProcessQueue();

// Assert
Assert.Equal(reentrantSync, capturedContext);
}
finally
{
SynchronizationContext.SetSynchronizationContext(null);
}
}

[Fact]

Check failure on line 1358 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

CenterEdge.Async.UnitTests.AsyncHelperTests.RunSync_TaskT_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue

Assert.Equal() Failure: Values differ Expected: ReentrantSynchronizationContext { } Actual: null

Check failure on line 1358 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

CenterEdge.Async.UnitTests.AsyncHelperTests.RunSync_TaskT_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue

Assert.Equal() Failure: Values differ Expected: ReentrantSynchronizationContext { } Actual: null
public void RunSync_TaskT_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue()
{
// Arrange
SynchronizationContext? capturedContext = null;

Check warning on line 1362 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 1362 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var reentrantSync = new ReentrantSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(reentrantSync);

try
{
// Act
var result = AsyncHelper.RunSync(() =>
{
#pragma warning disable CS4014
Task.Run(() =>
{
capturedContext = SynchronizationContext.Current;
});
#pragma warning restore CS4014

return Task.FromResult(42);
});

reentrantSync.ProcessQueue();

// Assert
Assert.Equal(42, result);
Assert.Equal(reentrantSync, capturedContext);
}
finally
{
SynchronizationContext.SetSynchronizationContext(null);
}
}

[Fact]

Check failure on line 1393 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

CenterEdge.Async.UnitTests.AsyncHelperTests.RunSync_ValueTaskT_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue

Assert.Equal() Failure: Values differ Expected: ReentrantSynchronizationContext { } Actual: null

Check failure on line 1393 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

CenterEdge.Async.UnitTests.AsyncHelperTests.RunSync_ValueTaskT_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue

Assert.Equal() Failure: Values differ Expected: ReentrantSynchronizationContext { } Actual: null
public void RunSync_ValueTaskT_SyncCompletionWithContinuations_RestoresSyncContextBeforeProcessingQueue()
{
// Arrange
SynchronizationContext? capturedContext = null;

Check warning on line 1397 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 1397 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var reentrantSync = new ReentrantSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(reentrantSync);

try
{
// Act
var result = AsyncHelper.RunSync(() =>
{
#pragma warning disable CS4014
Task.Run(() =>
{
capturedContext = SynchronizationContext.Current;
});
#pragma warning restore CS4014

return new ValueTask<int>(42);
});

reentrantSync.ProcessQueue();

// Assert
Assert.Equal(42, result);
Assert.Equal(reentrantSync, capturedContext);
}
finally
{
SynchronizationContext.SetSynchronizationContext(null);
}
}

#endregion

#region Helpers

private static readonly AsyncLocal<int> asyncLocalField = new();
Expand All @@ -1287,6 +1438,27 @@
action.Invoke();
}

// Simulates a reentrant synchronization context like WinForms
// that processes its message queue synchronously when asked
private class ReentrantSynchronizationContext : SynchronizationContext
{
private readonly Queue<(SendOrPostCallback, object?)> _queue = new();

Check warning on line 1445 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 1445 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

public override void Post(SendOrPostCallback d, object? state)

Check warning on line 1447 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 1447 in src/CenterEdge.Async.UnitTests/AsyncHelperTests.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
_queue.Enqueue((d, state));
}

public void ProcessQueue()
{
while (_queue.Count > 0)
{
var (callback, state) = _queue.Dequeue();
callback(state);
}
}
}

#endregion
}
}
16 changes: 16 additions & 0 deletions src/CenterEdge.Async/AsyncHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ public static void RunSync<TState>(Func<TState, Task> task, TState state)
}
else
{
// Restore the sync context before processing any queued continuations
// to avoid reentrancy issues where the parent context might process
// posted messages before we return
SynchronizationContext.SetSynchronizationContext(oldContext);
synch.RunAlreadyComplete();
}

Expand Down Expand Up @@ -168,6 +172,10 @@ public static void RunSync<TState>(Func<TState, ValueTask> task, TState state)
}
else
{
// Restore the sync context before processing any queued continuations
// to avoid reentrancy issues where the parent context might process
// posted messages before we return
SynchronizationContext.SetSynchronizationContext(oldContext);
synch.RunAlreadyComplete();
}

Expand Down Expand Up @@ -245,6 +253,10 @@ public static T RunSync<T, TState>(Func<TState, Task<T>> task, TState state)
}
else
{
// Restore the sync context before processing any queued continuations
// to avoid reentrancy issues where the parent context might process
// posted messages before we return
SynchronizationContext.SetSynchronizationContext(oldContext);
synch.RunAlreadyComplete();
}

Expand Down Expand Up @@ -326,6 +338,10 @@ public static T RunSync<T, TState>(Func<TState, ValueTask<T>> task, TState state
}
else
{
// Restore the sync context before processing any queued continuations
// to avoid reentrancy issues where the parent context might process
// posted messages before we return
SynchronizationContext.SetSynchronizationContext(oldContext);
synch.RunAlreadyComplete();
}

Expand Down
Loading