File: BackEnd\SdkResolverLoader_Tests.cs
Web Access
Project: ..\..\..\src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj (Microsoft.Build.Engine.UnitTests)
// 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.BackEnd.SdkResolution;
using Microsoft.Build.Construction;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
using Exception = System.Exception;
using SdkResolverBase = Microsoft.Build.Framework.SdkResolver;
using SdkResolverContextBase = Microsoft.Build.Framework.SdkResolverContext;
using SdkResultBase = Microsoft.Build.Framework.SdkResult;
using SdkResultFactoryBase = Microsoft.Build.Framework.SdkResultFactory;
 
#nullable disable
 
namespace Microsoft.Build.Engine.UnitTests.BackEnd
{
    public class SdkResolverLoader_Tests
    {
        private readonly ITestOutputHelper _output;
        private readonly MockLogger _logger;
 
        public SdkResolverLoader_Tests(ITestOutputHelper output)
        {
            _output = output;
            _logger = new MockLogger(output);
            ILoggingService loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1);
            loggingService.RegisterLogger(_logger);
        }
 
        [Fact]
        public void AssertDefaultLoaderReturnsDefaultResolvers()
        {
            var loader = new SdkResolverLoader();
 
            var resolvers = loader.LoadAllResolvers(new MockElementLocation("file"));
 
            resolvers.Select(i => i.GetType().FullName).ShouldBe(new[] { typeof(DefaultSdkResolver).FullName });
 
            _logger.ErrorCount.ShouldBe(0);
            _logger.WarningCount.ShouldBe(0);
        }
 
        [Fact]
        public void VerifySdkResolverLoaderFileDiscoveryPattern()
        {
            var root = FileUtilities.GetTemporaryDirectory();
            try
            {
                // Valid pattern is root\(Name)\(Name).dll. No other files should be considered.
                var d1 = Directory.CreateDirectory(Path.Combine(root, "Resolver1"));
 
                // Valid.
                var f1 = Path.Combine(d1.FullName, "Resolver1.dll");
 
                // Invalid, won't be considered.
                var f2 = Path.Combine(d1.FullName, "Dependency.dll");
                var f3 = Path.Combine(d1.FullName, "InvalidName.dll");
                var f4 = Path.Combine(d1.FullName, "NoResolver.txt");
 
                File.WriteAllText(f1, string.Empty);
                File.WriteAllText(f2, string.Empty);
                File.WriteAllText(f3, string.Empty);
                File.WriteAllText(f4, string.Empty);
 
                var strategy = new SdkResolverLoader();
                var files = strategy.FindPotentialSdkResolvers(root, new MockElementLocation("file"));
 
                files.Count.ShouldBe(1);
                files[0].ShouldBe(f1);
            }
            finally
            {
                FileUtilities.DeleteDirectoryNoThrow(root, true);
            }
        }
 
        [Fact]
        public void SdkResolverLoaderPrefersManifestFile()
        {
            var root = FileUtilities.GetTemporaryDirectory();
            try
            {
                var testFolder = Directory.CreateDirectory(Path.Combine(root, "MyTestResolver"));
 
                var wrongResolverDll = Path.Combine(testFolder.FullName, "MyTestResolver.dll");
                var resolverManifest = Path.Combine(testFolder.FullName, "MyTestResolver.xml");
                var assemblyToLoad = Path.Combine(root, "SomeOtherResolver.dll");
 
                File.WriteAllText(wrongResolverDll, string.Empty);
                File.WriteAllText(assemblyToLoad, string.Empty);
 
                File.WriteAllText(resolverManifest, $@"
                    <SdkResolver>
                      <Path>{assemblyToLoad}</Path>
                    </SdkResolver>");
 
                SdkResolverLoader loader = new SdkResolverLoader();
                var resolversFound = loader.FindPotentialSdkResolvers(root, new MockElementLocation("file"));
 
                resolversFound.Count.ShouldBe(1);
                resolversFound.First().ShouldBe(assemblyToLoad);
            }
            finally
            {
                FileUtilities.DeleteDirectoryNoThrow(root, true);
            }
        }
 
        /// <summary>
        /// Verifies that if an SDK resolver throws while creating an instance that a warning is logged.
        /// </summary>
        [Fact]
        public void VerifyThrowsWhenResolverFailsToLoad()
        {
            SdkResolverLoader sdkResolverLoader = new MockSdkResolverLoader
            {
                LoadResolverAssemblyFunc = (resolverPath) => typeof(SdkResolverLoader_Tests).GetTypeInfo().Assembly,
                FindPotentialSdkResolversFunc = (rootFolder, loc) => new List<string>
                {
                    "myresolver.dll"
                },
                GetResolverTypesFunc = assembly => new[] { typeof(MockSdkResolverThatDoesNotLoad) }
            };
 
            InvalidProjectFileException exception = Should.Throw<InvalidProjectFileException>(() =>
            {
                sdkResolverLoader.LoadAllResolvers(ElementLocation.EmptyLocation);
            });
 
            exception.Message.ShouldBe($"The SDK resolver type \"{nameof(MockSdkResolverThatDoesNotLoad)}\" failed to load. A8BB8B3131D3475D881ACD3AF8D75BD6");
 
            Exception innerException = exception.InnerException.ShouldBeOfType<Exception>();
 
            innerException.Message.ShouldBe(MockSdkResolverThatDoesNotLoad.ExpectedMessage);
 
            _logger.WarningCount.ShouldBe(0);
            _logger.ErrorCount.ShouldBe(0);
        }
 
        /// <summary>
        /// Verifies that when we attempt to create an instance of a resolver with no public constructor that a warning
        /// is logged with the appropriate message.
        /// </summary>
        [Fact]
        public void VerifyThrowsWhenResolverHasNoPublicConstructor()
        {
            SdkResolverLoader sdkResolverLoader = new MockSdkResolverLoader
            {
                LoadResolverAssemblyFunc = (resolverPath) => typeof(SdkResolverLoader_Tests).GetTypeInfo().Assembly,
                FindPotentialSdkResolversFunc = (rootFolder, loc) => new List<string>
                {
                    "myresolver.dll"
                },
                GetResolverTypesFunc = assembly => new[] { typeof(MockSdkResolverNoPublicConstructor) }
            };
 
            InvalidProjectFileException exception = Should.Throw<InvalidProjectFileException>(() =>
            {
                sdkResolverLoader.LoadAllResolvers(ElementLocation.EmptyLocation);
            });
 
            exception.Message.ShouldStartWith($"The SDK resolver type \"{nameof(MockSdkResolverNoPublicConstructor)}\" failed to load.");
 
            exception.InnerException.ShouldBeOfType<MissingMethodException>();
 
            _logger.WarningCount.ShouldBe(0);
            _logger.ErrorCount.ShouldBe(0);
        }
 
        /// <summary>
        /// Verifies that when a resolver assembly cannot be loaded, that a warning is logged and other resolvers are still loaded.
        /// </summary>
        [Fact]
        public void VerifyWarningLoggedWhenResolverAssemblyCannotBeLoaded()
        {
            const string assemblyPath = @"C:\foo\bar\myresolver.dll";
            const string expectedMessage = "91BF077D4E9646819DE7AB2CBA2637B6";
 
            SdkResolverLoader sdkResolverLoader = new MockSdkResolverLoader
            {
                LoadResolverAssemblyFunc = (resolverPath) => throw new Exception(expectedMessage),
                FindPotentialSdkResolversFunc = (rootFolder, loc) => new List<string>
                {
                    assemblyPath,
                }
            };
 
            InvalidProjectFileException exception = Should.Throw<InvalidProjectFileException>(() =>
            {
                sdkResolverLoader.LoadAllResolvers(ElementLocation.EmptyLocation);
            });
 
            exception.Message.ShouldBe($"The SDK resolver assembly \"{assemblyPath}\" could not be loaded. {expectedMessage}");
 
            Exception innerException = exception.InnerException.ShouldBeOfType<Exception>();
 
            innerException.Message.ShouldBe(expectedMessage);
 
            _logger.WarningCount.ShouldBe(0);
            _logger.ErrorCount.ShouldBe(0);
        }
 
        [Fact]
        public void SdkResolverLoaderReadsManifestFile()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                var root = env.CreateFolder().Path;
                var resolverPath = Path.Combine(root, "MyTestResolver");
                var resolverManifest = Path.Combine(resolverPath, "MyTestResolver.xml");
 
                var assemblyToLoad = env.CreateFile(".dll").Path;
 
                Directory.CreateDirectory(resolverPath);
                File.WriteAllText(resolverManifest, $@"
                    <SdkResolver>
                      <Path>{assemblyToLoad}</Path>
                    </SdkResolver>");
 
                SdkResolverLoader loader = new SdkResolverLoader();
                var resolversFound = loader.FindPotentialSdkResolvers(root, new MockElementLocation("file"));
 
                resolversFound.Count.ShouldBe(1);
                resolversFound.First().ShouldBe(assemblyToLoad);
            }
        }
 
        [Fact]
        public void SdkResolverLoaderReadsManifestFileWithResolvableSdkPattern()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                var root = env.CreateFolder().Path;
                var resolverPath = Path.Combine(root, "MyTestResolver");
                var resolverManifest = Path.Combine(resolverPath, "MyTestResolver.xml");
 
                var assemblyToLoad = env.CreateFile(".dll").Path;
 
                Directory.CreateDirectory(resolverPath);
                File.WriteAllText(resolverManifest, $@"
                    <SdkResolver>
                      <ResolvableSdkPattern>1&lt;.*</ResolvableSdkPattern>
                      <Path>{assemblyToLoad}</Path>
                    </SdkResolver>");
 
                SdkResolverLoader loader = new SdkResolverLoader();
                var resolversManifestsFound = loader.FindPotentialSdkResolversManifests(root, new MockElementLocation("file"));
 
                resolversManifestsFound.Count.ShouldBe(1);
                resolversManifestsFound.First().Path.ShouldBe(assemblyToLoad);
                resolversManifestsFound.First().ResolvableSdkRegex.ToString().ShouldBe("1<.*");
            }
        }
 
        [Fact]
        public void SdkResolverLoaderErrorsWithInvalidManifestFile()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                var root = env.CreateFolder().Path;
                var resolverPath = Path.Combine(root, "MyTestResolver");
                var resolverManifest = Path.Combine(resolverPath, "MyTestResolver.xml");
 
                var assemblyToLoad = env.CreateFile(".dll").Path;
 
                Directory.CreateDirectory(resolverPath);
                File.WriteAllText(resolverManifest, $@"
                    <SdkResolver2>
                      <Path>{assemblyToLoad}</Path>
                    </SdkResolver2>");
 
                SdkResolverLoader loader = new SdkResolverLoader();
 
                var ex = Should.Throw<InvalidProjectFileException>(() => loader.FindPotentialSdkResolvers(root, new MockElementLocation("file")));
                ex.ErrorCode.ShouldBe("MSB4245");
            }
        }
 
        [Fact]
        public void SdkResolverLoaderErrorsWhenNoDllOrAssemblyFound()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                var root = env.CreateFolder().Path;
                var resolverPath = Path.Combine(root, "MyTestResolver");
 
                Directory.CreateDirectory(resolverPath);
                SdkResolverLoader loader = new SdkResolverLoader();
 
                var ex = Should.Throw<InvalidProjectFileException>(() => loader.FindPotentialSdkResolvers(root, new MockElementLocation("file")));
                ex.ErrorCode.ShouldBe("MSB4246");
            }
        }
 
        [Fact]
        public void SdkResolverLoaderErrorsWhenManifestTargetMissing()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                var root = env.CreateFolder().Path;
                var resolverPath = Path.Combine(root, "MyTestResolver");
                var resolverManifest = Path.Combine(resolverPath, "MyTestResolver.xml");
 
                // Note this does NOT create the file just gets a valid name
                var assemblyToLoad = env.GetTempFile(".dll").Path;
 
                Directory.CreateDirectory(resolverPath);
                File.WriteAllText(resolverManifest, $@"
                    <SdkResolver>
                      <Path>{assemblyToLoad}</Path>
                    </SdkResolver>");
 
                SdkResolverLoader loader = new SdkResolverLoader();
                var ex = Should.Throw<InvalidProjectFileException>(() => loader.FindPotentialSdkResolvers(root, new MockElementLocation("file")));
                ex.ErrorCode.ShouldBe("MSB4247");
            }
        }
 
        [Fact]
        public void SdkResolverLoaderHonorsIncludeDefaultEnvVar()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                var origIncludeDefault = Environment.GetEnvironmentVariable("MSBUILDINCLUDEDEFAULTSDKRESOLVER");
                try
                {
                    var testRoot = env.CreateFolder().Path;
                    Environment.SetEnvironmentVariable("MSBUILDINCLUDEDEFAULTSDKRESOLVER", "false");
                    SdkResolverLoader loader = new MockSdkResolverLoader()
                    {
                        LoadResolversAction = (resolverPath, location, resolvers) =>
                        {
                            resolvers.Add(new MockSdkResolverWithAssemblyPath(resolverPath));
                        }
                    };
                    IReadOnlyList<SdkResolverBase> resolvers = loader.LoadAllResolvers(new MockElementLocation("file"));
 
                    resolvers.Count.ShouldBe(0);
                }
                finally
                {
                    Environment.SetEnvironmentVariable("MSBUILDINCLUDEDEFAULTSDKRESOLVER", origIncludeDefault);
                }
            }
        }
 
        [Fact]
        public void SdkResolverLoaderHonorsAdditionalResolversFolder()
        {
            using (var env = TestEnvironment.Create(_output))
            {
                var origResolversFolder = Environment.GetEnvironmentVariable("MSBUILDADDITIONALSDKRESOLVERSFOLDER");
                try
                {
                    var testRoot = env.CreateFolder().Path;
                    var additionalRoot = env.CreateFolder().Path;
 
                    var resolver1 = "Resolver1";
                    var resolver1Path = Path.Combine(additionalRoot, resolver1, $"{resolver1}.dll");
                    Directory.CreateDirectory(Path.Combine(testRoot, resolver1));
                    File.WriteAllText(Path.Combine(testRoot, resolver1, $"{resolver1}.dll"), string.Empty);
                    Directory.CreateDirectory(Path.Combine(additionalRoot, resolver1));
                    File.WriteAllText(resolver1Path, string.Empty);
                    var resolver2 = "Resolver2";
                    var resolver2Path = Path.Combine(testRoot, resolver2, $"{resolver2}.dll");
                    Directory.CreateDirectory(Path.Combine(testRoot, resolver2));
                    File.WriteAllText(resolver2Path, string.Empty);
                    var resolver3 = "Resolver3";
                    var resolver3Path = Path.Combine(additionalRoot, resolver3, $"{resolver3}.dll");
                    Directory.CreateDirectory(Path.Combine(additionalRoot, resolver3));
                    File.WriteAllText(resolver3Path, string.Empty);
 
                    Environment.SetEnvironmentVariable("MSBUILDADDITIONALSDKRESOLVERSFOLDER", additionalRoot);
 
                    SdkResolverLoader loader = new SdkResolverLoader();
                    IReadOnlyList<string> resolvers = loader.FindPotentialSdkResolvers(testRoot, new MockElementLocation("file"));
 
                    resolvers.ShouldBeSameIgnoringOrder(new[] { resolver1Path, resolver2Path, resolver3Path });
                }
                finally
                {
                    Environment.SetEnvironmentVariable("MSBUILDADDITIONALSDKRESOLVERSFOLDER", origResolversFolder);
                }
            }
        }
 
        private sealed class MockSdkResolverThatDoesNotLoad : SdkResolverBase
        {
            public const string ExpectedMessage = "A8BB8B3131D3475D881ACD3AF8D75BD6";
 
            public MockSdkResolverThatDoesNotLoad()
            {
                throw new Exception(ExpectedMessage);
            }
 
            public override string Name => nameof(MockSdkResolverThatDoesNotLoad);
 
            public override int Priority => 0;
 
            public override SdkResultBase Resolve(SdkReference sdkReference, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                throw new NotImplementedException();
            }
        }
 
        private sealed class MockSdkResolverNoPublicConstructor : SdkResolverBase
        {
            private MockSdkResolverNoPublicConstructor()
            {
            }
 
            public override string Name => nameof(MockSdkResolverNoPublicConstructor);
 
            public override int Priority => 0;
 
            public override SdkResultBase Resolve(SdkReference sdkReference, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                throw new NotImplementedException();
            }
        }
 
        private sealed class MockSdkResolverWithAssemblyPath : SdkResolverBase
        {
            public string AssemblyPath;
 
            public MockSdkResolverWithAssemblyPath(string assemblyPath = "")
            {
                AssemblyPath = assemblyPath;
            }
 
            public override string Name => nameof(MockSdkResolverWithAssemblyPath);
 
            public override int Priority => 0;
 
            public override SdkResultBase Resolve(SdkReference sdkReference, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                throw new NotImplementedException();
            }
        }
 
        private sealed class MockSdkResolverLoader : SdkResolverLoader
        {
            public Func<string, Assembly> LoadResolverAssemblyFunc { get; set; }
 
            public Func<string, ElementLocation, IReadOnlyList<string>> FindPotentialSdkResolversFunc { get; set; }
 
            public Func<Assembly, IEnumerable<Type>> GetResolverTypesFunc { get; set; }
 
            public Action<string, ElementLocation, List<SdkResolver>> LoadResolversAction { get; set; }
 
            protected override Assembly LoadResolverAssembly(string resolverPath)
            {
                if (LoadResolverAssemblyFunc != null)
                {
                    return LoadResolverAssemblyFunc(resolverPath);
                }
 
                return base.LoadResolverAssembly(resolverPath);
            }
 
            protected override IEnumerable<Type> GetResolverTypes(Assembly assembly)
            {
                if (GetResolverTypesFunc != null)
                {
                    return GetResolverTypesFunc(assembly);
                }
 
                return base.GetResolverTypes(assembly);
            }
 
            internal override IReadOnlyList<string> FindPotentialSdkResolvers(string rootFolder, ElementLocation location)
            {
                if (FindPotentialSdkResolversFunc != null)
                {
                    return FindPotentialSdkResolversFunc(rootFolder, location);
                }
 
                return base.FindPotentialSdkResolvers(rootFolder, location);
            }
 
            protected override void LoadResolvers(string resolverPath, ElementLocation location, List<SdkResolver> resolvers)
            {
                if (LoadResolversAction != null)
                {
                    LoadResolversAction(resolverPath, location, resolvers);
                    return;
                }
                base.LoadResolvers(resolverPath, location, resolvers);
            }
        }
    }
}