271 lines
10 KiB
C#
271 lines
10 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Testing.xunit;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AspNetCore.Razor.Tools
|
|
{
|
|
public class ServerLifecycleTest
|
|
{
|
|
private static ServerRequest EmptyServerRequest => new ServerRequest(1, Array.Empty<RequestArgument>());
|
|
|
|
private static ServerResponse EmptyServerResponse => new CompletedServerResponse(
|
|
returnCode: 0,
|
|
utf8output: false,
|
|
output: 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);
|
|
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;
|
|
});
|
|
|
|
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>
|
|
// Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991
|
|
[ConditionalFact]
|
|
[OSSkipCondition(OperatingSystems.Linux)]
|
|
[OSSkipCondition(OperatingSystems.MacOSX)]
|
|
public async Task ServerRunning_ShutdownRequest_DoesNotAbortCompilation()
|
|
{
|
|
// Arrange
|
|
var completionSource = new TaskCompletionSource<bool>();
|
|
var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
|
|
{
|
|
// We want this to keep running even after the shutdown is seen.
|
|
completionSource.Task.Wait();
|
|
return EmptyServerResponse;
|
|
});
|
|
|
|
using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
|
|
{
|
|
var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
|
|
|
|
// Act
|
|
// The compilation is now in progress, send the shutdown.
|
|
await ServerUtilities.SendShutdown(serverData.PipeName);
|
|
Assert.False(compileTask.IsCompleted);
|
|
|
|
// Now let the task complete.
|
|
completionSource.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>
|
|
// Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991
|
|
[ConditionalFact]
|
|
[OSSkipCondition(OperatingSystems.Linux)]
|
|
[OSSkipCondition(OperatingSystems.MacOSX)]
|
|
public async Task ServerRunning_MultipleShutdownRequests_HandlesSuccessfully()
|
|
{
|
|
// Arrange
|
|
var completionSource = new TaskCompletionSource<bool>();
|
|
var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
|
|
{
|
|
// We want this to keep running even after the shutdown is seen.
|
|
completionSource.Task.Wait();
|
|
return EmptyServerResponse;
|
|
});
|
|
|
|
using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
|
|
{
|
|
var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
|
|
|
|
// 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.
|
|
completionSource.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);
|
|
}
|
|
}
|
|
|
|
// Skipping temporarily on non-windows. https://github.com/aspnet/Razor/issues/1991
|
|
[ConditionalFact]
|
|
[OSSkipCondition(OperatingSystems.Linux)]
|
|
[OSSkipCondition(OperatingSystems.MacOSX)]
|
|
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();
|
|
});
|
|
|
|
using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
|
|
{
|
|
var tasks = new List<Task<ServerResponse>>();
|
|
for (var i = 0; i < requestCount; i++)
|
|
{
|
|
var task = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
|
|
tasks.Add(task);
|
|
}
|
|
|
|
// Act
|
|
// Wait until all of the connections are being processed by the server.
|
|
completionSource.Task.Wait();
|
|
|
|
// Now cancel
|
|
var stats = await serverData.CancelAndCompleteAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(requestCount, stats.Connections);
|
|
Assert.Equal(requestCount, count);
|
|
|
|
foreach (var task in tasks)
|
|
{
|
|
// We expect this to throw because the stream is already closed.
|
|
await Assert.ThrowsAsync<EndOfStreamException>(() => task);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|