File: ServerLifecycleTest.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Sdk.Razor.Tool.Tests\Microsoft.NET.Sdk.Razor.Tool.Tests.csproj (Microsoft.NET.Sdk.Razor.Tool.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Diagnostics;
using Moq;
 
namespace Microsoft.NET.Sdk.Razor.Tool.Tests
{
    public class ServerLifecycleTest
    {
        private static ServerRequest EmptyServerRequest => new(1, Array.Empty<RequestArgument>());
 
        private static ServerResponse EmptyServerResponse => new CompletedServerResponse(
            returnCode: 0,
            utf8output: false,
            output: string.Empty,
            error: string.Empty);
 
        [Fact]
        public void ServerStartup_MutexAlreadyAcquired_Fails()
        {
            // Arrange
            var pipeName = Guid.NewGuid().ToString("N");
            var mutexName = MutexName.GetServerMutexName(pipeName);
            var compilerHost = new Mock<CompilerHost>(MockBehavior.Strict);
            var host = new Mock<ConnectionHost>(MockBehavior.Strict);
 
            // Act & Assert
            using (var mutex = new Mutex(initiallyOwned: true, name: mutexName, createdNew: out var holdsMutex))
            {
                Assert.True(holdsMutex);
                try
                {
                    var result = ServerUtilities.RunServer(pipeName, host.Object, compilerHost.Object);
 
                    // Assert failure
                    Assert.Equal(1, result);
                }
                finally
                {
                    mutex.ReleaseMutex();
                }
            }
        }
 
        [Fact]
        public void ServerStartup_SuccessfullyAcquiredMutex()
        {
            // Arrange, Act & Assert
            var pipeName = Guid.NewGuid().ToString("N");
            var mutexName = MutexName.GetServerMutexName(pipeName);
            var compilerHost = new Mock<CompilerHost>(MockBehavior.Strict);
            var host = new Mock<ConnectionHost>(MockBehavior.Strict);
#pragma warning disable xUnit1031
            host
                .Setup(x => x.WaitForConnectionAsync(It.IsAny<CancellationToken>()))
                .Returns(() =>
                {
                    // Use a thread instead of Task to guarantee this code runs on a different
                    // thread and we can validate the mutex state. 
                    var source = new TaskCompletionSource<bool>();
                    var thread = new Thread(_ =>
                    {
                        Mutex mutex = null;
                        try
                        {
                            Assert.True(Mutex.TryOpenExisting(mutexName, out mutex));
                            Assert.False(mutex.WaitOne(millisecondsTimeout: 0));
                            source.SetResult(true);
                        }
                        catch (Exception ex)
                        {
                            source.SetException(ex);
                            throw;
                        }
                        finally
                        {
                            mutex?.Dispose();
                        }
                    });
 
                    // Synchronously wait here.  Don't returned a Task value because we need to 
                    // ensure the above check completes before the server hits a timeout and 
                    // releases the mutex. 
                    thread.Start();
                    source.Task.Wait();
 
                    return new TaskCompletionSource<Connection>().Task;
                });
#pragma warning restore xUnit1031
 
            var result = ServerUtilities.RunServer(pipeName, host.Object, compilerHost.Object, keepAlive: TimeSpan.FromSeconds(1));
            Assert.Equal(0, result);
        }
 
        [Fact]
        public async Task ServerRunning_ShutdownRequest_processesSuccessfully()
        {
            // Arrange
            using (var serverData = ServerUtilities.CreateServer())
            {
                // Act
                var serverProcessId = await ServerUtilities.SendShutdown(serverData.PipeName);
 
                // Assert
                Assert.Equal(Process.GetCurrentProcess().Id, serverProcessId);
                await serverData.Verify(connections: 1, completed: 1);
            }
        }
 
        /// <summary>
        /// A shutdown request should not abort an existing compilation.  It should be allowed to run to 
        /// completion.
        /// </summary>
        [Fact]
        public async Task ServerRunning_ShutdownRequest_DoesNotAbortCompilation()
        {
            // Arrange
            var startCompilationSource = new TaskCompletionSource<bool>();
            var finishCompilationSource = new TaskCompletionSource<bool>();
#pragma warning disable xUnit1031
            var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
            {
                // At this point, the connection has been accepted and the compilation has started.
                startCompilationSource.SetResult(true);
 
                // We want this to keep running even after the shutdown is seen.
                finishCompilationSource.Task.Wait();
                return EmptyServerResponse;
            });
#pragma warning restore xUnit1031
 
            using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
            {
                var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
 
                // Wait for the request to go through and trigger compilation.
                await startCompilationSource.Task;
 
                // Act
                // The compilation is now in progress, send the shutdown.
                await ServerUtilities.SendShutdown(serverData.PipeName);
                Assert.False(compileTask.IsCompleted);
 
                // Now let the task complete.
                finishCompilationSource.SetResult(true);
 
                // Assert
                var response = await compileTask;
                Assert.Equal(ServerResponse.ResponseType.Completed, response.Type);
                Assert.Equal(0, ((CompletedServerResponse)response).ReturnCode);
 
                await serverData.Verify(connections: 2, completed: 2);
            }
        }
 
        /// <summary>
        /// Multiple clients should be able to send shutdown requests to the server.
        /// </summary>
        [Fact]
        public async Task ServerRunning_MultipleShutdownRequests_HandlesSuccessfully()
        {
            // Arrange
            var startCompilationSource = new TaskCompletionSource<bool>();
            var finishCompilationSource = new TaskCompletionSource<bool>();
#pragma warning disable xUnit1031
            var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
            {
                // At this point, the connection has been accepted and the compilation has started.
                startCompilationSource.SetResult(true);
 
                // We want this to keep running even after the shutdown is seen.
                finishCompilationSource.Task.Wait();
                return EmptyServerResponse;
            });
#pragma warning restore xUnit1031
 
            using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
            {
                var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
 
                // Wait for the request to go through and trigger compilation.
                await startCompilationSource.Task;
 
                // Act
                for (var i = 0; i < 10; i++)
                {
                    // The compilation is now in progress, send the shutdown.
                    var processId = await ServerUtilities.SendShutdown(serverData.PipeName);
                    Assert.Equal(Process.GetCurrentProcess().Id, processId);
                    Assert.False(compileTask.IsCompleted);
                }
 
                // Now let the task complete.
                finishCompilationSource.SetResult(true);
 
                // Assert
                var response = await compileTask;
                Assert.Equal(ServerResponse.ResponseType.Completed, response.Type);
                Assert.Equal(0, ((CompletedServerResponse)response).ReturnCode);
 
                await serverData.Verify(connections: 11, completed: 11);
            }
        }
 
        // https://github.com/aspnet/Razor/issues/1991
        [WindowsOnlyFact]
        public async Task ServerRunning_CancelCompilation_CancelsSuccessfully()
        {
            // Arrange
            const int requestCount = 5;
            var count = 0;
            var completionSource = new TaskCompletionSource<bool>();
            var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
            {
                if (Interlocked.Increment(ref count) == requestCount)
                {
                    completionSource.SetResult(true);
                }
 
                ct.WaitHandle.WaitOne();
                return new RejectedServerResponse();
            });
 
            var semaphore = new SemaphoreSlim(1);
            Action<object, EventArgs> onListening = (s, e) =>
            {
                semaphore.Release();
            };
            using (var serverData = ServerUtilities.CreateServer(compilerHost: host, onListening: onListening))
            {
                // Send all the requests.
                var clients = new List<Client>();
                for (var i = 0; i < requestCount; i++)
                {
                    // Wait for the server to start listening.
                    await semaphore.WaitAsync(TimeSpan.FromMinutes(1));
 
                    var client = await Client.ConnectAsync(serverData.PipeName, timeout: null, cancellationToken: default);
                    await EmptyServerRequest.WriteAsync(client.Stream);
                    clients.Add(client);
                }
 
                // Act
                // Wait until all of the connections are being processed by the server. 
                await completionSource.Task;
 
                // Now cancel
                var stats = await serverData.CancelAndCompleteAsync();
 
                // Assert
                Assert.Equal(requestCount, stats.Connections);
                Assert.Equal(requestCount, count);
 
                // Read the server response to each client.
                foreach (var client in clients)
                {
                    var task = ServerResponse.ReadAsync(client.Stream);
                    // We expect this to throw because the stream is already closed.
                    await Assert.ThrowsAnyAsync<IOException>(() => task);
                    client.Dispose();
                }
            }
        }
 
        private static TestableCompilerHost CreateCompilerHost(Action<TestableCompilerHost> configureCompilerHost = null)
        {
            var compilerHost = new TestableCompilerHost();
            configureCompilerHost?.Invoke(compilerHost);
 
            return compilerHost;
        }
 
        private class TestableCompilerHost : CompilerHost
        {
            internal Func<ServerRequest, CancellationToken, ServerResponse> ExecuteFunc;
 
            public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken)
            {
                if (ExecuteFunc != null)
                {
                    return ExecuteFunc(request, cancellationToken);
                }
 
                return EmptyServerResponse;
            }
        }
    }
}