|
// 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
extern alias csc;
extern alias vbc;
using System;
using System.IO;
using Microsoft.CodeAnalysis.CommandLine;
using System.Runtime.InteropServices;
using Moq;
using Xunit;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using System.Threading;
using System.IO.Pipes;
using System.Security.AccessControl;
using System.Security.Principal;
using Xunit.Abstractions;
namespace Microsoft.CodeAnalysis.CompilerServer.UnitTests
{
public abstract class BuildClientTests : TestBase
{
public sealed class ServerTests : BuildClientTests
{
private readonly string _pipeName = Guid.NewGuid().ToString("N");
private readonly BuildPaths _buildPaths;
private readonly List<ServerData> _serverDataList = new List<ServerData>();
private readonly XunitCompilerServerLogger _logger;
public ServerTests(ITestOutputHelper testOutputHelper)
{
_buildPaths = ServerUtil.CreateBuildPaths(
workingDir: Temp.CreateDirectory().Path,
tempDir: Temp.CreateDirectory().Path);
_logger = new XunitCompilerServerLogger(testOutputHelper);
}
public override void Dispose()
{
foreach (var serverData in _serverDataList)
{
serverData.CancellationTokenSource.Cancel();
serverData.ServerTask.Wait();
}
base.Dispose();
}
private BuildClient CreateClient(
RequestLanguage? language = null,
CompileFunc compileFunc = null,
CompileOnServerFunc compileOnServerFunc = null)
{
language ??= RequestLanguage.CSharpCompile;
compileFunc ??= delegate { return 0; };
compileOnServerFunc ??= delegate { throw new InvalidOperationException(); };
return new BuildClient(_logger, language.Value, compileFunc, compileOnServerFunc);
}
private ServerData CreateServer(string pipeName)
{
var serverData = ServerUtil.CreateServer(_logger, pipeName).GetAwaiter().GetResult();
_serverDataList.Add(serverData);
return serverData;
}
[Fact]
public void ConnectToServerFails()
{
// Create and grab the mutex for the server. This should make
// the client believe that a server is active and it will try
// to connect. When it fails it should fall back to in-proc
// compilation.
bool holdsMutex;
using (var serverMutex = BuildServerConnection.OpenOrCreateMutex(
name: BuildServerConnection.GetServerMutexName(_pipeName),
createdNew: out holdsMutex))
{
Assert.True(holdsMutex);
var ranLocal = false;
// Note: Connecting to a server can take up to a second to time out
var client = CreateClient(
compileFunc: delegate
{
ranLocal = true;
return 0;
});
var exitCode = client.RunCompilation(new[] { "/shared" }, _buildPaths, pipeName: _pipeName).ExitCode;
Assert.Equal(0, exitCode);
Assert.True(ranLocal);
}
}
#if NET472
[Fact]
public void TestMutexConstructorException()
{
using (var outer = new Mutex(initiallyOwned: true, name: BuildServerConnection.GetClientMutexName(_pipeName), out bool createdNew))
{
Assert.True(createdNew);
var mutexSecurity = outer.GetAccessControl();
mutexSecurity.AddAccessRule(new MutexAccessRule(WindowsIdentity.GetCurrent().Owner, MutexRights.FullControl, AccessControlType.Deny));
outer.SetAccessControl(mutexSecurity);
var ranLocal = false;
var client = CreateClient(
compileFunc: delegate
{
ranLocal = true;
return 0;
});
var exitCode = client.RunCompilation(new[] { "/shared" }, _buildPaths, pipeName: _pipeName).ExitCode;
Assert.Equal(0, exitCode);
Assert.True(ranLocal);
}
}
#endif
[Fact]
[WorkItem(70166, "https://github.com/dotnet/roslyn/issues/70166")]
public void TestIfMutexIsGlobal()
{
const string GlobalPrefix = "Global\\";
var clientMutexName = BuildServerConnection.GetClientMutexName(_pipeName);
Assert.True(clientMutexName.StartsWith(GlobalPrefix));
var serverMutexName = BuildServerConnection.GetServerMutexName(_pipeName);
Assert.True(serverMutexName.StartsWith(GlobalPrefix));
}
[Fact]
public async Task ConnectToPipe()
{
string pipeName = ServerUtil.GetPipeName();
var oneSec = TimeSpan.FromSeconds(1);
Assert.False(await tryConnectToNamedPipe((int)oneSec.TotalMilliseconds, cancellationToken: default));
// Try again with infinite timeout and cancel
var cts = new CancellationTokenSource();
var connection = tryConnectToNamedPipe(Timeout.Infinite, cts.Token);
Assert.False(connection.IsCompleted);
cts.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(
async () => await connection);
// Create server and try again
using var serverData = CreateServer(pipeName);
Assert.True(await tryConnectToNamedPipe(Timeout.Infinite, cancellationToken: default));
async Task<bool> tryConnectToNamedPipe(int timeoutMs, CancellationToken cancellationToken)
{
using var pipeStream = await BuildServerConnection.TryConnectToServerAsync(pipeName, timeoutMs, _logger, cancellationToken);
return pipeStream != null;
}
}
[Fact]
public void FallbackToCsc()
{
var ranLocal = false;
var ranServer = false;
var client = CreateClient(
compileFunc: (_, _, _, _) =>
{
ranLocal = true;
return 0;
},
compileOnServerFunc: (_, _, _) =>
{
ranServer = true;
return Task.FromResult<BuildResponse>(new RejectedBuildResponse(""));
});
var exitCode = client.RunCompilation(new[] { "/shared" }, _buildPaths, pipeName: _pipeName).ExitCode;
Assert.Equal(0, exitCode);
Assert.True(ranLocal);
Assert.True(ranServer);
Assert.Equal(0, _serverDataList.Count);
}
}
public sealed class TryParseClientArgsTest : BuildClientTests
{
private bool _hasShared;
private string _keepAlive;
private string _errorMessage;
private string _sessionKey;
private List<string> _parsedArgs;
private bool Parse(params string[] args)
{
return CommandLineParser.TryParseClientArgs(
args,
out _parsedArgs,
out _hasShared,
out _keepAlive,
out _sessionKey,
out _errorMessage);
}
[Theory]
[InlineData('-')]
[InlineData('/')]
public void Shared(char optionPrefix)
{
Assert.True(Parse(optionPrefix + "shared", "test.cs"));
Assert.True(_hasShared);
Assert.Null(_sessionKey);
Assert.Equal(new[] { "test.cs" }, _parsedArgs);
}
[Theory]
[InlineData('-')]
[InlineData('/')]
public void SharedWithSessionKey(char optionPrefix)
{
Assert.True(Parse(optionPrefix + "shared:pipe", "test.cs"));
Assert.True(_hasShared);
Assert.Equal("pipe", _sessionKey);
Assert.Equal(new[] { "test.cs" }, _parsedArgs);
Assert.True(Parse(optionPrefix + "shared:1:2", "test.cs"));
Assert.True(_hasShared);
Assert.Equal("1:2", _sessionKey);
Assert.Equal(new[] { "test.cs" }, _parsedArgs);
Assert.True(Parse(optionPrefix + "shared=1:2", "test.cs"));
Assert.True(_hasShared);
Assert.Equal("1:2", _sessionKey);
Assert.Equal(new[] { "test.cs" }, _parsedArgs);
}
[Theory]
[InlineData('-')]
[InlineData('/')]
public void SharedWithEmptySessionKey(char optionPrefix)
{
Assert.False(Parse(optionPrefix + "shared:", "test.cs"));
Assert.False(_hasShared);
Assert.Equal(CodeAnalysisResources.SharedArgumentMissing, _errorMessage);
}
[Theory]
[InlineData('-')]
[InlineData('/')]
public void SharedPrefix(char optionPrefix)
{
Assert.True(Parse(optionPrefix + "sharedstart", "test.cs"));
Assert.False(_hasShared);
Assert.Equal(new[] { optionPrefix + "sharedstart", "test.cs" }, _parsedArgs);
}
[Theory]
[InlineData('-')]
[InlineData('/')]
public void Basic(char optionPrefix)
{
Assert.True(Parse("test.cs"));
Assert.False(_hasShared);
Assert.Null(_sessionKey);
Assert.Equal(new[] { "test.cs" }, _parsedArgs);
Assert.True(Parse(optionPrefix + "keepalive:100", "/shared", "test.cs"));
Assert.True(_hasShared);
Assert.Null(_sessionKey);
Assert.Equal("100", _keepAlive);
Assert.Equal(new[] { "test.cs" }, _parsedArgs);
}
[Theory]
[InlineData('-')]
[InlineData('/')]
public void KeepAliveBad(char optionPrefix)
{
Assert.False(Parse(optionPrefix + "keepalive", "test.cs"));
Assert.Equal(CodeAnalysisResources.MissingKeepAlive, _errorMessage);
Assert.False(Parse(optionPrefix + "keepalive:", "test.cs"));
Assert.Equal(CodeAnalysisResources.MissingKeepAlive, _errorMessage);
Assert.False(Parse(optionPrefix + "keepalive:-100", "test.cs"));
Assert.Equal(CodeAnalysisResources.KeepAliveIsTooSmall, _errorMessage);
Assert.False(Parse(optionPrefix + "keepalive:100", "test.cs"));
Assert.Equal(CodeAnalysisResources.KeepAliveWithoutShared, _errorMessage);
}
[Theory]
[InlineData('-')]
[InlineData('/')]
public void KeepAlivePrefix(char optionPrefix)
{
Assert.True(Parse(optionPrefix + "keepalivestart", "test.cs"));
Assert.Null(_keepAlive);
Assert.Equal(new[] { optionPrefix + "keepalivestart", "test.cs" }, _parsedArgs);
}
[Theory]
[InlineData('-')]
[InlineData('/')]
public void KeepAlive(char optionPrefix)
{
Assert.True(Parse(optionPrefix + "keepalive:100", optionPrefix + "shared", "test.cs"));
Assert.Equal("100", _keepAlive);
Assert.Equal(new[] { "test.cs" }, _parsedArgs);
Assert.True(_hasShared);
Assert.Null(_sessionKey);
Assert.True(Parse(optionPrefix + "keepalive=100", optionPrefix + "shared", "test.cs"));
Assert.Equal("100", _keepAlive);
Assert.Equal(new[] { "test.cs" }, _parsedArgs);
Assert.True(_hasShared);
Assert.Null(_sessionKey);
}
}
public class MiscTest
{
[Fact]
public void GetPipeNameForPathOptSlashes()
{
var path = string.Format(@"q:{0}the{0}path", Path.DirectorySeparatorChar);
var name = BuildServerConnection.GetPipeName(path);
Assert.Equal(name, BuildServerConnection.GetPipeName(path));
Assert.Equal(name, BuildServerConnection.GetPipeName(path + Path.DirectorySeparatorChar));
Assert.Equal(name, BuildServerConnection.GetPipeName(path + Path.DirectorySeparatorChar + Path.DirectorySeparatorChar));
}
[Fact]
public void GetPipeNameForPathOptLength()
{
var path = string.Format(@"q:{0}the{0}path", Path.DirectorySeparatorChar);
var name = BuildServerConnection.GetPipeName(path);
// We only have ~50 total bytes to work with on mac, so the base path must be small
Assert.Equal(43, name.Length);
}
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/75714")]
public void GetPipeNameForPath_Casing()
{
var path1 = string.Format(@"q:{0}the{0}path", Path.DirectorySeparatorChar);
var path2 = string.Format(@"Q:{0}The{0}Path", Path.DirectorySeparatorChar);
Assert.Equal(BuildServerConnection.GetPipeName(path1), BuildServerConnection.GetPipeName(path2));
}
}
}
}
|