File: Utilities\AsynchronousOperationListenerTests.cs
Web Access
Project: src\src\EditorFeatures\Test\Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.EditorFeatures.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.UnitTests.Utilities;
 
public class AsynchronousOperationListenerTests
{
    /// <summary>
    /// A delay which is not expected to be reached in practice.
    /// </summary>
    private static readonly TimeSpan s_unexpectedDelay = TimeSpan.FromSeconds(60);
 
    private class SleepHelper : IDisposable
    {
        private readonly CancellationTokenSource _tokenSource;
        private readonly List<Task> _tasks = [];
 
        public SleepHelper()
            => _tokenSource = new CancellationTokenSource();
 
        public void Dispose()
        {
            Task[] tasks;
            lock (_tasks)
            {
                _tokenSource.Cancel();
                tasks = [.. _tasks];
            }
 
            try
            {
                Task.WaitAll(tasks);
            }
            catch (AggregateException e)
            {
                foreach (var inner in e.InnerExceptions)
                {
                    Assert.IsType<TaskCanceledException>(inner);
                }
            }
 
            GC.SuppressFinalize(this);
        }
 
        public void Sleep(TimeSpan timeToSleep)
        {
            Task task;
            lock (_tasks)
            {
                task = Task.Factory.StartNew(() =>
                {
                    while (true)
                    {
                        _tokenSource.Token.ThrowIfCancellationRequested();
                        Thread.Sleep(TimeSpan.FromMilliseconds(10));
                    }
                }, _tokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
 
                _tasks.Add(task);
            }
 
            task.Wait((int)timeToSleep.TotalMilliseconds);
        }
    }
 
    [Fact]
    public void Operation()
    {
        using var sleepHelper = new SleepHelper();
        var signal = new ManualResetEventSlim();
        var listener = new AsynchronousOperationListener();
 
        var done = false;
        var asyncToken = listener.BeginAsyncOperation("Test");
        var task = new Task(() =>
            {
                signal.Set();
                sleepHelper.Sleep(TimeSpan.FromSeconds(1));
                done = true;
            });
        task.CompletesAsyncOperation(asyncToken);
        task.Start(TaskScheduler.Default);
 
        Wait(listener, signal);
        Assert.True(done, "The operation should have completed");
    }
 
    [Fact]
    public void QueuedOperation()
    {
        using var sleepHelper = new SleepHelper();
        var signal = new ManualResetEventSlim();
        var listener = new AsynchronousOperationListener();
 
        var done = false;
        var asyncToken1 = listener.BeginAsyncOperation("Test");
        var task = new Task(() =>
            {
                signal.Set();
                sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
 
                var asyncToken2 = listener.BeginAsyncOperation("Test");
                var queuedTask = new Task(() =>
                    {
                        sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
                        done = true;
                    });
                queuedTask.CompletesAsyncOperation(asyncToken2);
                queuedTask.Start(TaskScheduler.Default);
            });
 
        task.CompletesAsyncOperation(asyncToken1);
        task.Start(TaskScheduler.Default);
 
        Wait(listener, signal);
 
        Assert.True(done, "Should have waited for the queued operation to finish!");
    }
 
    [Fact]
    public void Cancel()
    {
        using var sleepHelper = new SleepHelper();
        var signal = new ManualResetEventSlim();
        var listener = new AsynchronousOperationListener();
 
        var done = false;
        var continued = false;
        var asyncToken1 = listener.BeginAsyncOperation("Test");
        var task = new Task(() =>
            {
                signal.Set();
                sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
                var asyncToken2 = listener.BeginAsyncOperation("Test");
                var queuedTask = new Task(() =>
                    {
                        sleepHelper.Sleep(s_unexpectedDelay);
                        continued = true;
                    });
                asyncToken2.Dispose();
                queuedTask.Start(TaskScheduler.Default);
                done = true;
            });
        task.CompletesAsyncOperation(asyncToken1);
        task.Start(TaskScheduler.Default);
 
        Wait(listener, signal);
 
        Assert.True(done, "Cancelling should have completed the current task.");
        Assert.False(continued, "Continued Task when it shouldn't have.");
    }
 
    [Fact]
    public void Nested()
    {
        using var sleepHelper = new SleepHelper();
        var signal = new ManualResetEventSlim();
        var listener = new AsynchronousOperationListener();
 
        var outerDone = false;
        var innerDone = false;
        var asyncToken1 = listener.BeginAsyncOperation("Test");
        var task = new Task(() =>
            {
                signal.Set();
                sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
 
                using (listener.BeginAsyncOperation("Test"))
                {
                    sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
                    innerDone = true;
                }
 
                sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
                outerDone = true;
            });
        task.CompletesAsyncOperation(asyncToken1);
        task.Start(TaskScheduler.Default);
 
        Wait(listener, signal);
 
        Assert.True(innerDone, "Should have completed the inner task");
        Assert.True(outerDone, "Should have completed the outer task");
    }
 
    [Fact]
    public void MultipleEnqueues()
    {
        using var sleepHelper = new SleepHelper();
        var signal = new ManualResetEventSlim();
        var listener = new AsynchronousOperationListener();
 
        var outerDone = false;
        var firstQueuedDone = false;
        var secondQueuedDone = false;
 
        var asyncToken1 = listener.BeginAsyncOperation("Test");
        var task = new Task(() =>
            {
                signal.Set();
                sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
 
                var asyncToken2 = listener.BeginAsyncOperation("Test");
                var firstQueueTask = new Task(() =>
                    {
                        sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
                        var asyncToken3 = listener.BeginAsyncOperation("Test");
                        var secondQueueTask = new Task(() =>
                            {
                                sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
                                secondQueuedDone = true;
                            });
                        secondQueueTask.CompletesAsyncOperation(asyncToken3);
                        secondQueueTask.Start(TaskScheduler.Default);
                        firstQueuedDone = true;
                    });
                firstQueueTask.CompletesAsyncOperation(asyncToken2);
                firstQueueTask.Start(TaskScheduler.Default);
                outerDone = true;
            });
        task.CompletesAsyncOperation(asyncToken1);
        task.Start(TaskScheduler.Default);
 
        Wait(listener, signal);
 
        Assert.True(outerDone, "The outer task should have finished!");
        Assert.True(firstQueuedDone, "The first queued task should have finished");
        Assert.True(secondQueuedDone, "The second queued task should have finished");
    }
 
    [Fact]
    public void IgnoredCancel()
    {
        using var sleepHelper = new SleepHelper();
        var signal = new ManualResetEventSlim();
        var listener = new AsynchronousOperationListener();
 
        var done = new TaskCompletionSource<VoidResult>();
        var queuedFinished = new TaskCompletionSource<VoidResult>();
        var cancelledFinished = new TaskCompletionSource<VoidResult>();
        var asyncToken1 = listener.BeginAsyncOperation("Test");
        var task = new Task(() =>
        {
            using (listener.BeginAsyncOperation("Test"))
            {
                var cancelledTask = new Task(() =>
                {
                    sleepHelper.Sleep(TimeSpan.FromSeconds(10));
                    cancelledFinished.SetResult(default);
                });
 
                signal.Set();
                cancelledTask.Start(TaskScheduler.Default);
            }
 
            // Now that we've canceled the first request, queue another one to make sure we wait for it.
            var asyncToken2 = listener.BeginAsyncOperation("Test");
            var queuedTask = new Task(() =>
            {
                queuedFinished.SetResult(default);
            });
            queuedTask.CompletesAsyncOperation(asyncToken2);
            queuedTask.Start(TaskScheduler.Default);
            done.SetResult(default);
        });
        task.CompletesAsyncOperation(asyncToken1);
        task.Start(TaskScheduler.Default);
 
        Wait(listener, signal);
 
        Assert.True(done.Task.IsCompleted, "Cancelling should have completed the current task.");
        Assert.True(queuedFinished.Task.IsCompleted, "Continued didn't run, but it was supposed to ignore the cancel.");
        Assert.False(cancelledFinished.Task.IsCompleted, "We waited for the cancelled task to finish.");
    }
 
    [Fact]
    public void SecondCompletion()
    {
        using var sleepHelper = new SleepHelper();
        var signal1 = new ManualResetEventSlim();
        var signal2 = new ManualResetEventSlim();
        var listener = new AsynchronousOperationListener();
 
        var firstDone = false;
        var secondDone = false;
 
        var asyncToken1 = listener.BeginAsyncOperation("Test");
        var firstTask = Task.Factory.StartNew(() =>
            {
                signal1.Set();
                sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
                firstDone = true;
            }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
        firstTask.CompletesAsyncOperation(asyncToken1);
        firstTask.Wait();
 
        var asyncToken2 = listener.BeginAsyncOperation("Test");
        var secondTask = Task.Factory.StartNew(() =>
            {
                signal2.Set();
                sleepHelper.Sleep(TimeSpan.FromMilliseconds(500));
                secondDone = true;
            }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
        secondTask.CompletesAsyncOperation(asyncToken2);
 
        // give it two signals since second one might not have started when WaitTask.Wait is called - race condition
        Wait(listener, signal1, signal2);
 
        Assert.True(firstDone, "First didn't finish");
        Assert.True(secondDone, "Should have waited for the second task");
    }
 
    private static void Wait(AsynchronousOperationListener listener, ManualResetEventSlim signal)
    {
        // Note: WaitTask will return immediately if there is no outstanding work.  Due to
        // threadpool scheduling, we may get here before that other thread has started to run.
        // That's why each task set's a signal to say that it has begun and we first wait for
        // that, and then start waiting.
        Assert.True(signal.Wait(s_unexpectedDelay), "Shouldn't have hit timeout waiting for task to begin");
        var waitTask = listener.ExpeditedWaitAsync();
        Assert.True(waitTask.Wait(s_unexpectedDelay), "Wait shouldn't have needed to timeout");
    }
 
    private static void Wait(AsynchronousOperationListener listener, ManualResetEventSlim signal1, ManualResetEventSlim signal2)
    {
        // Note: WaitTask will return immediately if there is no outstanding work.  Due to
        // threadpool scheduling, we may get here before that other thread has started to run.
        // That's why each task set's a signal to say that it has begun and we first wait for
        // that, and then start waiting.
        Assert.True(signal1.Wait(s_unexpectedDelay), "Shouldn't have hit timeout waiting for task to begin");
        Assert.True(signal2.Wait(s_unexpectedDelay), "Shouldn't have hit timeout waiting for task to begin");
 
        var waitTask = listener.ExpeditedWaitAsync();
        Assert.True(waitTask.Wait(s_unexpectedDelay), "Wait shouldn't have needed to timeout");
    }
}