|
// 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 Basic.Reference.Assemblies;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit.Abstractions;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests
{
public sealed class ExportProviderBuilderTests(ITestOutputHelper testOutputHelper)
: AbstractLanguageServerHostTests(testOutputHelper)
{
[Fact]
public async Task MefCompositionIsCached()
{
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);
await AssertCacheWriteWasAttemptedAsync();
AssertCachedCompositionCountEquals(expectedCount: 1);
}
[Fact]
public async Task MefCompositionIsReused()
{
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);
await AssertCacheWriteWasAttemptedAsync();
// Second test server with the same set of assemblies.
await using var testServer2 = await CreateLanguageServerAsync(includeDevKitComponents: false);
AssertNoCacheWriteWasAttempted();
AssertCachedCompositionCountEquals(expectedCount: 1);
}
[Fact]
public async Task MultipleMefCompositionsAreCached()
{
await using var testServer = await CreateLanguageServerAsync(includeDevKitComponents: false);
await AssertCacheWriteWasAttemptedAsync();
// Second test server with a different set of assemblies.
await using var testServer2 = await CreateLanguageServerAsync(includeDevKitComponents: true);
await AssertCacheWriteWasAttemptedAsync();
AssertCachedCompositionCountEquals(expectedCount: 2);
}
[Theory, WorkItem("https://github.com/microsoft/vscode-dotnettools/issues/1686")]
// gen-delims
[InlineData("#"),
InlineData(":"),
InlineData("?"),
InlineData("["),
InlineData("]"),
InlineData("@"),
// sub-delims
InlineData("!"),
InlineData("$"),
InlineData("&"),
InlineData("'"),
InlineData("("),
InlineData(")"),
InlineData("*"),
InlineData("+"),
InlineData(","),
InlineData(";"),
InlineData("=")]
public async Task CanFindCodeBaseWhenReservedCharactersInPath(string reservedCharacter)
{
// Test that given an unescaped code base URI (as vs-mef gives us), we can resolve the file path even if it contains reserved characters.
// Certain characters aren't valid file paths on different file systems and so can't be in the path.
if (ExecutionConditionUtil.IsWindows && reservedCharacter is "*" or ":" or "?")
{
return;
}
var dllPath = GenerateDll(reservedCharacter, out var assemblyName);
await using var testServer = await TestLspServer.CreateAsync(new Roslyn.LanguageServer.Protocol.ClientCapabilities(), TestOutputLogger, MefCacheDirectory.Path, includeDevKitComponents: true, [dllPath]);
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var assembly = Assert.Single(assemblies, a => a.GetName().Name == assemblyName);
var type = Assert.Single(assembly.GetTypes(), t => t.FullName?.Contains("ExportedType") == true);
var values = testServer.ExportProvider.GetExportedValues(type, contractName: null);
Assert.Single(values);
}
private string GenerateDll(string reservedCharacter, out string assemblyName)
{
var directory = TempRoot.CreateDirectory();
CSharpCompilationOptions options = new(OutputKind.DynamicallyLinkedLibrary);
// Create a dll that exports and imports a mef type to ensure that MEF attempts to load and create a MEF graph
// using this assembly.
var source = """
namespace MyTestExportNamespace
{
[System.ComponentModel.Composition.Export(typeof(ExportedType))]
public class ExportedType { }
public class ImportType
{
[System.ComponentModel.Composition.ImportingConstructorAttribute]
public ImportType(ExportedType t) { }
}
}
""";
// Generate an assembly name associated with the character we're testing - this ensures
// that if multiple of these tests run in the same process we're making sure that the correct expected assembly is loaded.
assemblyName = "MyAssembly" + reservedCharacter.GetHashCode();
#pragma warning disable RS0030 // Do not use banned APIs - intentionally using System.ComponentModel.Composition to verify mef construction.
var compilation = CSharpCompilation.Create(
assemblyName,
syntaxTrees: [SyntaxFactory.ParseSyntaxTree(SourceText.From(source, encoding: null, SourceHashAlgorithms.Default))],
references:
[
NetStandard20.References.mscorlib,
NetStandard20.References.netstandard,
NetStandard20.References.SystemRuntime,
MetadataReference.CreateFromFile(typeof(System.ComponentModel.Composition.ExportAttribute).Assembly.Location)
],
options: options);
#pragma warning restore RS0030 // Do not use banned APIs
// Write a dll to a subdir with a reserved character in the name.
var tempSubDir = directory.CreateDirectory(reservedCharacter);
var tempFile = tempSubDir.CreateFile($"{assemblyName}.dll");
var dllData = compilation.EmitToStream();
tempFile.WriteAllBytes(dllData.ToArray());
// Mark the file as read only to prevent mutations.
var fileInfo = new FileInfo(tempFile.Path);
fileInfo.IsReadOnly = true;
return tempFile.Path;
}
private async Task AssertCacheWriteWasAttemptedAsync()
{
var cacheWriteTask = ExportProviderBuilder.TestAccessor.GetCacheWriteTask();
Assert.NotNull(cacheWriteTask);
await cacheWriteTask;
}
private void AssertNoCacheWriteWasAttempted()
{
var cacheWriteTask2 = ExportProviderBuilder.TestAccessor.GetCacheWriteTask();
Assert.Null(cacheWriteTask2);
}
private void AssertCachedCompositionCountEquals(int expectedCount)
{
var mefCompositions = Directory.EnumerateFiles(MefCacheDirectory.Path, "*.mef-composition", SearchOption.AllDirectories);
Assert.Equal(expectedCount, mefCompositions.Count());
}
}
}
|