|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests.Shared;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
#nullable disable
namespace Microsoft.Build.UnitTests
{
public class TypeLoader_Tests
{
private static readonly string ProjectFileFolder = Path.Combine(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, "PortableTask");
private const string ProjectFileName = "portableTaskTest.proj";
private const string DLLFileName = "PortableTask.dll";
private static string PortableTaskFolderPath = Path.GetFullPath(
Path.Combine(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, "..", "..", "..", "Samples", "PortableTask"));
private readonly ITestOutputHelper _output;
public TypeLoader_Tests(ITestOutputHelper testOutputHelper)
{
_output = testOutputHelper;
}
[Fact]
public void Basic()
{
Assert.True(TypeLoader.IsPartialTypeNameMatch("Csc", "csc")); // ==> exact match
Assert.True(TypeLoader.IsPartialTypeNameMatch("Microsoft.Build.Tasks.Csc", "Microsoft.Build.Tasks.Csc")); // ==> exact match
Assert.True(TypeLoader.IsPartialTypeNameMatch("Microsoft.Build.Tasks.Csc", "Csc")); // ==> partial match
Assert.True(TypeLoader.IsPartialTypeNameMatch("Microsoft.Build.Tasks.Csc", "Tasks.Csc")); // ==> partial match
Assert.True(TypeLoader.IsPartialTypeNameMatch("MyTasks.ATask+NestedTask", "NestedTask")); // ==> partial match
Assert.True(TypeLoader.IsPartialTypeNameMatch("MyTasks.ATask\\\\+NestedTask", "NestedTask")); // ==> partial match
Assert.False(TypeLoader.IsPartialTypeNameMatch("MyTasks.CscTask", "Csc")); // ==> no match
Assert.False(TypeLoader.IsPartialTypeNameMatch("MyTasks.MyCsc", "Csc")); // ==> no match
Assert.False(TypeLoader.IsPartialTypeNameMatch("MyTasks.ATask\\.Csc", "Csc")); // ==> no match
Assert.False(TypeLoader.IsPartialTypeNameMatch("MyTasks.ATask\\\\\\.Csc", "Csc")); // ==> no match
}
[Fact]
public void Regress_Mutation_TrailingPartMustMatch()
{
Assert.False(TypeLoader.IsPartialTypeNameMatch("Microsoft.Build.Tasks.Csc", "Vbc"));
}
[Fact]
public void Regress_Mutation_ParameterOrderDoesntMatter()
{
Assert.True(TypeLoader.IsPartialTypeNameMatch("Csc", "Microsoft.Build.Tasks.Csc"));
}
[Fact]
public void LoadNonExistingAssembly()
{
using var dir = new FileUtilities.TempWorkingDirectory(ProjectFileFolder);
string projectFilePath = Path.Combine(dir.Path, ProjectFileName);
string dllName = "NonExistent.dll";
bool successfulExit;
string output = RunnerUtilities.ExecMSBuild(projectFilePath + " /v:diag /p:AssemblyPath=" + dllName, out successfulExit, _output);
successfulExit.ShouldBeFalse();
string dllPath = Path.Combine(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, dllName);
CheckIfCorrectAssemblyLoaded(output, dllPath, false);
}
[Fact]
public void LoadInsideAsssembly()
{
using (var dir = new FileUtilities.TempWorkingDirectory(ProjectFileFolder))
{
string projectFilePath = Path.Combine(dir.Path, ProjectFileName);
bool successfulExit;
string output = RunnerUtilities.ExecMSBuild(projectFilePath + " /v:diag", out successfulExit, _output);
Assert.True(successfulExit);
string dllPath = Path.Combine(dir.Path, DLLFileName);
CheckIfCorrectAssemblyLoaded(output, dllPath);
}
}
[Fact]
public void LoadTaskDependingOnMSBuild()
{
using (TestEnvironment env = TestEnvironment.Create())
{
TransientTestFolder folder = env.CreateFolder(createFolder: true);
string currentAssembly = Assembly.GetExecutingAssembly().Location;
string utilitiesName = "Microsoft.Build.Utilities.Core.dll";
string newAssemblyLocation = Path.Combine(folder.Path, Path.GetFileName(currentAssembly));
// The "first" directory is "Debug" or "Release"
string portableTaskPath = Path.Combine(Directory.GetDirectories(PortableTaskFolderPath).First(), "netstandard2.0", "OldMSBuild");
string utilities = Path.Combine(portableTaskPath, utilitiesName);
File.Copy(utilities, Path.Combine(folder.Path, utilitiesName));
File.Copy(currentAssembly, newAssemblyLocation);
TypeLoader typeLoader = new(TaskLoader.IsTaskClass);
// If we cannot accept MSBuild next to the task assembly we're loading, this will throw.
typeLoader.Load("TypeLoader_Tests", AssemblyLoadInfo.Create(null, newAssemblyLocation), useTaskHost: true);
}
}
[Fact]
public void LoadOutsideAssembly()
{
using (var dir = new FileUtilities.TempWorkingDirectory(ProjectFileFolder))
{
string projectFilePath = Path.Combine(dir.Path, ProjectFileName);
string originalDLLPath = Path.Combine(dir.Path, DLLFileName);
string movedDLLPath = MoveOrCopyDllToTempDir(originalDLLPath, copy: false);
try
{
bool successfulExit;
string output = RunnerUtilities.ExecMSBuild(projectFilePath + " /v:diag /p:AssemblyPath=" + movedDLLPath, out successfulExit, _output);
Assert.True(successfulExit);
CheckIfCorrectAssemblyLoaded(output, movedDLLPath);
}
finally
{
UndoDLLOperations(movedDLLPath);
}
}
}
[Fact(Skip = "https://github.com/dotnet/msbuild/issues/325")]
public void LoadInsideAssemblyWhenGivenOutsideAssemblyWithSameName()
{
using (var dir = new FileUtilities.TempWorkingDirectory(ProjectFileFolder))
{
string projectFilePath = Path.Combine(dir.Path, ProjectFileName);
string originalDLLPath = Path.Combine(dir.Path, DLLFileName);
string copiedDllPath = MoveOrCopyDllToTempDir(originalDLLPath, copy: true);
try
{
bool successfulExit;
string output = RunnerUtilities.ExecMSBuild(projectFilePath + " /v:diag /p:AssemblyPath=" + copiedDllPath, out successfulExit, _output);
Assert.True(successfulExit);
CheckIfCorrectAssemblyLoaded(output, originalDLLPath);
}
finally
{
UndoDLLOperations(copiedDllPath);
}
}
}
/// <summary>
/// </summary>
/// <param name="copy"></param>
/// <returns>Path to new DLL</returns>
private string MoveOrCopyDllToTempDir(string originalDllPath, bool copy)
{
var temporaryDirectory = FileUtilities.GetTemporaryDirectory();
var newDllPath = Path.Combine(temporaryDirectory, DLLFileName);
Assert.True(File.Exists(originalDllPath));
if (copy)
{
File.Copy(originalDllPath, newDllPath);
Assert.True(File.Exists(newDllPath));
}
else
{
File.Move(originalDllPath, newDllPath);
Assert.True(File.Exists(newDllPath));
Assert.False(File.Exists(originalDllPath));
}
return newDllPath;
}
/// <summary>
/// Delete newDllPath and delete temp directory
/// </summary>
/// <param name="newDllPath"></param>
/// <param name="moveBack">If true, move newDllPath back to bin. If false, delete it</param>
private void UndoDLLOperations(string newDllPath)
{
var tempDirectoryPath = Path.GetDirectoryName(newDllPath);
File.Delete(newDllPath);
Assert.False(File.Exists(newDllPath));
Assert.Empty(Directory.EnumerateFiles(tempDirectoryPath));
Directory.Delete(tempDirectoryPath);
Assert.False(Directory.Exists(tempDirectoryPath));
}
private void CheckIfCorrectAssemblyLoaded(string scriptOutput, string expectedAssemblyPath, bool expectedSuccess = true)
{
var successfulMessage = @"Using ""ShowItems"" task from assembly """ + expectedAssemblyPath + @""".";
if (expectedSuccess)
{
Assert.Contains(successfulMessage, scriptOutput, StringComparison.OrdinalIgnoreCase);
}
else
{
Assert.DoesNotContain(successfulMessage, scriptOutput, StringComparison.OrdinalIgnoreCase);
}
}
#if FEATURE_ASSEMBLY_LOCATION
/// <summary>
/// Make sure that when we load multiple types out of the same assembly with different type filters that both the fullyqualified name matching and the
/// partial name matching still work.
/// </summary>
[Fact]
public void Regress640476PartialName()
{
string forwardingLoggerLocation = typeof(Microsoft.Build.Logging.ConfigurableForwardingLogger).Assembly.Location;
TypeLoader loader = new TypeLoader(IsForwardingLoggerClass);
LoadedType loadedType = loader.Load("ConfigurableForwardingLogger", AssemblyLoadInfo.Create(null, forwardingLoggerLocation));
Assert.NotNull(loadedType);
Assert.Equal(forwardingLoggerLocation, loadedType.Assembly.AssemblyLocation);
string fileLoggerLocation = typeof(Microsoft.Build.Logging.FileLogger).Assembly.Location;
loader = new TypeLoader(IsLoggerClass);
loadedType = loader.Load("FileLogger", AssemblyLoadInfo.Create(null, fileLoggerLocation));
Assert.NotNull(loadedType);
Assert.Equal(fileLoggerLocation, loadedType.Assembly.AssemblyLocation);
}
/// <summary>
/// Make sure that when we load multiple types out of the same assembly with different type filters that both the fullyqualified name matching and the
/// partial name matching still work.
/// </summary>
[Fact]
public void Regress640476FullyQualifiedName()
{
Type forwardingLoggerType = typeof(Microsoft.Build.Logging.ConfigurableForwardingLogger);
string forwardingLoggerLocation = forwardingLoggerType.Assembly.Location;
TypeLoader loader = new TypeLoader(IsForwardingLoggerClass);
LoadedType loadedType = loader.Load(forwardingLoggerType.FullName, AssemblyLoadInfo.Create(null, forwardingLoggerLocation));
Assert.NotNull(loadedType);
Assert.Equal(forwardingLoggerLocation, loadedType.Assembly.AssemblyLocation);
Type fileLoggerType = typeof(Microsoft.Build.Logging.FileLogger);
string fileLoggerLocation = fileLoggerType.Assembly.Location;
loader = new TypeLoader(IsLoggerClass);
loadedType = loader.Load(fileLoggerType.FullName, AssemblyLoadInfo.Create(null, fileLoggerLocation));
Assert.NotNull(loadedType);
Assert.Equal(fileLoggerLocation, loadedType.Assembly.AssemblyLocation);
}
/// <summary>
/// Make sure if no typeName is passed in then pick the first type which matches the desired type filter.
/// This has been in since whidbey but there has been no test for it and it was broken in the last refactoring of TypeLoader.
/// This test is to prevent that from happening again.
/// </summary>
[Fact]
public void NoTypeNamePicksFirstType()
{
Type forwardingLoggerType = typeof(Microsoft.Build.Logging.ConfigurableForwardingLogger);
string forwardingLoggerAssemblyLocation = forwardingLoggerType.Assembly.Location;
Func<Type, object, bool> forwardingLoggerfilter = IsForwardingLoggerClass;
Type firstPublicType = FirstPublicDesiredType(forwardingLoggerfilter, forwardingLoggerAssemblyLocation);
TypeLoader loader = new TypeLoader(forwardingLoggerfilter);
LoadedType loadedType = loader.Load(String.Empty, AssemblyLoadInfo.Create(null, forwardingLoggerAssemblyLocation));
Assert.NotNull(loadedType);
Assert.Equal(forwardingLoggerAssemblyLocation, loadedType.Assembly.AssemblyLocation);
Assert.Equal(firstPublicType, loadedType.Type);
Type fileLoggerType = typeof(Microsoft.Build.Logging.FileLogger);
string fileLoggerAssemblyLocation = forwardingLoggerType.Assembly.Location;
Func<Type, object, bool> fileLoggerfilter = IsLoggerClass;
firstPublicType = FirstPublicDesiredType(fileLoggerfilter, fileLoggerAssemblyLocation);
loader = new TypeLoader(fileLoggerfilter);
loadedType = loader.Load(String.Empty, AssemblyLoadInfo.Create(null, fileLoggerAssemblyLocation));
Assert.NotNull(loadedType);
Assert.Equal(fileLoggerAssemblyLocation, loadedType.Assembly.AssemblyLocation);
Assert.Equal(firstPublicType, loadedType.Type);
}
private static Type FirstPublicDesiredType(Func<Type, object, bool> filter, string assemblyLocation)
{
Assembly loadedAssembly = Assembly.UnsafeLoadFrom(assemblyLocation);
// only look at public types
Type[] allPublicTypesInAssembly = loadedAssembly.GetExportedTypes();
foreach (Type publicType in allPublicTypesInAssembly)
{
if (filter(publicType, null))
{
return publicType;
}
}
return null;
}
private static bool IsLoggerClass(Type type, object unused)
{
return type.IsClass &&
!type.IsAbstract &&
(type.GetInterface("ILogger") != null);
}
private static bool IsForwardingLoggerClass(Type type, object unused)
{
return type.IsClass &&
!type.IsAbstract &&
(type.GetInterface("IForwardingLogger") != null);
}
#endif
}
}
|