File: Ordering\RequestOrderingTests.cs
Web Access
Project: src\src\LanguageServer\ProtocolUnitTests\Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol.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.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Xunit.Abstractions;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.RequestOrdering
{
    public partial class RequestOrderingTests : AbstractLanguageServerProtocolTests
    {
        public RequestOrderingTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
        {
        }
 
        protected override TestComposition Composition => base.Composition
            .AddParts(typeof(MutatingRequestHandler))
            .AddParts(typeof(NonMutatingRequestHandler))
            .AddParts(typeof(FailingRequestHandler))
            .AddParts(typeof(FailingMutatingRequestHandler))
            .AddParts(typeof(NonLSPSolutionRequestHandler))
            .AddParts(typeof(LongRunningNonMutatingRequestHandler));
 
        [Theory, CombinatorialData]
        public async Task MutatingRequestsDoNotOverlap(bool mutatingLspWorkspace)
        {
            var requests = new[] {
                new TestRequest(MutatingRequestHandler.MethodName),
                new TestRequest(MutatingRequestHandler.MethodName),
                new TestRequest(MutatingRequestHandler.MethodName),
            };
 
            await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace);
            var responses = await TestAsync(testLspServer, requests);
 
            // Every request should have started at or after the one before it
            Assert.True(responses[1].StartTime >= responses[0].EndTime);
            Assert.True(responses[2].StartTime >= responses[1].EndTime);
        }
 
        [Theory, CombinatorialData]
        public async Task NonMutatingRequestsOverlap(bool mutatingLspWorkspace)
        {
            var requests = new[] {
                new TestRequest(NonMutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
            };
 
            await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace);
            var responses = await TestAsync(testLspServer, requests);
 
            // Every request should have started immediately, without waiting
            Assert.True(responses[1].StartTime < responses[0].EndTime);
            Assert.True(responses[2].StartTime < responses[1].EndTime);
        }
 
        [Theory, CombinatorialData]
        public async Task NonMutatingWaitsForMutating(bool mutatingLspWorkspace)
        {
            var requests = new[] {
                new TestRequest(MutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
            };
 
            await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace);
            var responses = await TestAsync(testLspServer, requests);
 
            // The non mutating tasks should have waited for the first task to finish
            Assert.True(responses[1].StartTime >= responses[0].EndTime);
            Assert.True(responses[2].StartTime >= responses[0].EndTime);
            // The non mutating requests shouldn't have waited for each other
            Assert.True(responses[1].StartTime < responses[2].EndTime);
            Assert.True(responses[2].StartTime < responses[1].EndTime);
        }
 
        [Theory, CombinatorialData]
        public async Task MutatingDoesntWaitForNonMutating(bool mutatingLspWorkspace)
        {
            var requests = new[] {
                new TestRequest(NonMutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
                new TestRequest(MutatingRequestHandler.MethodName),
            };
 
            await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace);
            var responses = await TestAsync(testLspServer, requests);
 
            // All tasks should start without waiting for any to finish
            Assert.True(responses[1].StartTime < responses[0].EndTime);
            Assert.True(responses[2].StartTime < responses[0].EndTime);
            Assert.True(responses[1].StartTime < responses[2].EndTime);
            Assert.True(responses[2].StartTime < responses[1].EndTime);
        }
 
        [Theory, CombinatorialData]
        public async Task ThrowingTaskDoesntBringDownQueue(bool mutatingLspWorkspace)
        {
            var requests = new[] {
                new TestRequest(FailingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
                new TestRequest(MutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
            };
 
            await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace);
            var waitables = StartTestRun(testLspServer, requests);
 
            // first task should fail
            await Assert.ThrowsAsync<StreamJsonRpc.RemoteInvocationException>(() => waitables[0]);
 
            // remaining tasks should have executed normally
            var responses = await Task.WhenAll(waitables.Skip(1));
 
            Assert.Empty(responses.Where(r => r == null));
            Assert.Empty(responses.Where(r => r!.StartTime == default));
            Assert.All(responses, r => Assert.True(r!.EndTime > r!.StartTime));
        }
 
        [Theory, CombinatorialData]
        public async Task LongRunningSynchronousNonMutatingTaskDoesNotBlockQueue(bool mutatingLspWorkspace)
        {
            var requests = new[] {
                new TestRequest(LongRunningNonMutatingRequestHandler.MethodName),
                new TestRequest(MutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
            };
 
            await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace);
 
            // Cancel all requests if the request queue is blocked for 1 minute. This will result in a failed test run.
            using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1));
            var waitables = StartTestRun(testLspServer, requests, cts.Token);
 
            // Non-long running tasks should run and complete. If there's a test-failure for a "cancellation"
            // at this point it means our long running task blocked the queue and prevented completion.
            var responses = await Task.WhenAll(waitables.Skip(1));
            Assert.Empty(responses.Where(r => r == null));
            Assert.Empty(responses.Where(r => r!.StartTime == default));
            Assert.All(responses, r => Assert.True(r!.EndTime > r!.StartTime));
 
            // Our long-running waitable should still be running until cancelled.
            var longRunningWaitable = waitables[0];
            Assert.False(longRunningWaitable.IsCompleted);
        }
 
        [Theory, CombinatorialData]
        public async Task FailingMutableTaskShutsDownQueue(bool mutatingLspWorkspace)
        {
            // NOTE: A failing task shuts down the queue not due to an exception escaping out of the handler
            //       but because the solution state would be invalid. This doesn't test the queues exception
            //       resiliancy.
 
            var requests = new[] {
                new TestRequest(FailingMutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
                new TestRequest(MutatingRequestHandler.MethodName),
                new TestRequest(NonMutatingRequestHandler.MethodName),
            };
 
            await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace);
            var waitables = StartTestRun(testLspServer, requests);
 
            // first task should fail
            await Assert.ThrowsAsync<StreamJsonRpc.RemoteInvocationException>(() => waitables[0]);
 
            // The failed request returns to the client before the shutdown completes.
            // Wait for the queue to finish handling the failed request and shutdown.
            await testLspServer.GetQueueAccessor()!.Value.WaitForProcessingToStopAsync().ConfigureAwait(false);
 
            // remaining tasks should be canceled
            var areAllItemsCancelled = await testLspServer.GetQueueAccessor()!.Value.AreAllItemsCancelledUnsafeAsync();
            Assert.True(areAllItemsCancelled);
        }
 
        [Theory, CombinatorialData]
        public async Task NonMutatingRequestsOperateOnTheSameSolutionAfterMutation(bool mutatingLspWorkspace)
        {
            await using var testLspServer = await CreateTestLspServerAsync("class C { {|caret:|} }", mutatingLspWorkspace);
 
            var expectedSolution = testLspServer.GetCurrentSolution();
 
            // solution should be the same because no mutations have happened
            var solution = await GetLSPSolution(testLspServer, NonMutatingRequestHandler.MethodName);
            Assert.Equal(expectedSolution, solution);
 
            // Open a document, to get a forked solution
            await ExecuteDidOpen(testLspServer, testLspServer.GetLocations("caret").First().Uri);
 
            // solution should be different because there has been a mutation
            solution = await GetLSPSolution(testLspServer, NonMutatingRequestHandler.MethodName);
            Assert.NotEqual(expectedSolution, solution);
 
            expectedSolution = solution;
 
            // solution should be the same because no mutations have happened
            solution = await GetLSPSolution(testLspServer, NonMutatingRequestHandler.MethodName);
            Assert.Equal(expectedSolution, solution);
 
            // Apply some random change to the workspace that the LSP server doesn't "see"
            testLspServer.TestWorkspace.SetCurrentSolution(s => s.WithProjectName(s.Projects.First().Id, "NewName"), WorkspaceChangeKind.ProjectChanged);
 
            expectedSolution = testLspServer.GetCurrentSolution();
 
            solution = await GetLSPSolution(testLspServer, NonMutatingRequestHandler.MethodName);
 
            if (mutatingLspWorkspace)
            {
                // In the case where this is a mutating workspace, we would expect the solutions to be the same as
                // everything pushes through between the manager and the workspace, and there have been no text changes
                // in one to cause us to fork.
                Assert.Equal(expectedSolution, solution);
            }
            else
            {
                // in the case where this is a non-mutating solution, the solutions should be different because there
                // has been a workspace change.
                Assert.NotEqual(expectedSolution, solution);
            }
 
            expectedSolution = solution;
 
            // solution should be the same because no mutations have happened
            solution = await GetLSPSolution(testLspServer, NonMutatingRequestHandler.MethodName);
            Assert.Equal(expectedSolution, solution);
        }
 
        [Theory, CombinatorialData]
        public async Task HandlerThatSkipsBuildingLSPSolutionGetsWorkspaceSolution(bool mutatingLspWorkspace)
        {
            await using var testLspServer = await CreateTestLspServerAsync("class C { {|caret:|} }", mutatingLspWorkspace);
 
            var solution = await GetLSPSolution(testLspServer, NonLSPSolutionRequestHandler.MethodName);
            Assert.Null(solution);
 
            // Open a document, to create a change that LSP handlers wouldn normally see
            await ExecuteDidOpen(testLspServer, testLspServer.GetLocations("caret").First().Uri);
 
            // solution shouldn't have changed
            solution = await GetLSPSolution(testLspServer, NonLSPSolutionRequestHandler.MethodName);
            Assert.Null(solution);
        }
 
        private static async Task ExecuteDidOpen(TestLspServer testLspServer, Uri documentUri)
        {
            var didOpenParams = new LSP.DidOpenTextDocumentParams
            {
                TextDocument = new LSP.TextDocumentItem
                {
                    Uri = documentUri,
                    Text = "// hi there"
                }
            };
            await testLspServer.ExecuteRequestAsync<LSP.DidOpenTextDocumentParams, object>(Methods.TextDocumentDidOpenName, didOpenParams, CancellationToken.None);
        }
 
        private static async Task<Solution?> GetLSPSolution(TestLspServer testLspServer, string methodName)
        {
            var request = new TestRequest(methodName);
            var response = await testLspServer.ExecuteRequestAsync<TestRequest, TestResponse>(request.MethodName, request, CancellationToken.None);
            Contract.ThrowIfNull(response);
            if (response.ContextHasSolution)
            {
                var (_, solution) = await testLspServer.GetManager().GetLspSolutionInfoAsync(CancellationToken.None).ConfigureAwait(false);
                Contract.ThrowIfNull(solution);
                return solution;
            }
 
            return null;
        }
 
        private static async Task<TestResponse[]> TestAsync(TestLspServer testLspServer, TestRequest[] requests)
        {
            var waitables = StartTestRun(testLspServer, requests);
 
            var responses = await Task.WhenAll(waitables);
 
            // Sanity checks to ensure test handlers aren't doing something wacky, making future checks invalid
            Assert.Empty(responses.Where(r => r == null));
            Assert.Empty(responses.Where(r => r!.StartTime == default));
            Assert.All(responses, r => Assert.True(r!.EndTime > r!.StartTime));
 
            return responses!;
        }
 
        private static List<Task<TestResponse?>> StartTestRun(TestLspServer testLspServer, TestRequest[] requests, CancellationToken cancellationToken = default)
        {
            var waitables = new List<Task<TestResponse?>>();
            foreach (var request in requests)
                waitables.Add(testLspServer.ExecuteRequestAsync<TestRequest, TestResponse>(request.MethodName, request, cancellationToken));
 
            return waitables;
        }
    }
}