File: BackEnd\SdkResolverService_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.Diagnostics.Tracing;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.BackEnd.SdkResolution;
using Microsoft.Build.Construction;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Unittest;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;
using SdkResolverContextBase = Microsoft.Build.Framework.SdkResolverContext;
using SdkResultBase = Microsoft.Build.Framework.SdkResult;
using SdkResultFactoryBase = Microsoft.Build.Framework.SdkResultFactory;
using SdkResultImpl = Microsoft.Build.BackEnd.SdkResolution.SdkResult;
 
#nullable disable
 
namespace Microsoft.Build.Engine.UnitTests.BackEnd
{
    public class SdkResolverService_Tests : IDisposable
    {
        private readonly MockLogger _logger;
        private readonly LoggingContext _loggingContext;
 
        public SdkResolverService_Tests()
        {
            _logger = new MockLogger();
            ILoggingService loggingService = LoggingService.CreateLoggingService(LoggerMode.Synchronous, 1);
            loggingService.RegisterLogger(_logger);
 
            _loggingContext = new MockLoggingContext(
                loggingService,
                new BuildEventContext(0, 0, BuildEventContext.InvalidProjectContextId, 0, 0));
        }
 
        public void Dispose()
        {
            SdkResolverService.Instance.InitializeForTests();
        }
 
        [Fact]
        // Scenario: Sdk is not resolved.
        public void AssertAllResolverErrorsLoggedWhenSdkNotResolved()
        {
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(includeResolversWithPatterns: true));
 
            SdkReference sdk = new SdkReference("notfound", "referencedVersion", "minimumVersion");
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Success.ShouldBeFalse();
            result.ShouldNotBeNull();
            result.SdkReference.ShouldNotBeNull();
            result.SdkReference.Name.ShouldBe("notfound");
            result.SdkReference.Version.ShouldBe("referencedVersion");
            result.SdkReference.MinimumVersion.ShouldBe("minimumVersion");
 
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolver1 running");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolver2 running");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldNotContain("MockSdkResolverWithResolvableSdkPattern1 running");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolverWithResolvableSdkPattern2 running");
 
            // First error is a generic "we failed" message.
            _logger.Errors[0].Message.ShouldBe(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("FailedToResolveSDK", "notfound", string.Join($"{Environment.NewLine}  ", new[] {
                "ERROR4",
                ResourceUtilities.FormatResourceStringStripCodeAndKeyword("SDKResolverReturnedNull", "MockResolverReturnsNull"),
                "ERROR1",
                "ERROR2",
                "notfound"
            })));
            _logger.Warnings.Select(i => i.Message).ShouldBe(new[] { "WARNING4", "WARNING2" });
        }
 
        [Fact]
        public void AssertResolutionWarnsIfResolvedVersionIsDifferentFromReferencedVersion()
        {
            var sdk = new SdkReference("foo", "1.0.0", null);
 
            SdkResolverService.Instance.InitializeForTests(
                null,
                new List<SdkResolver>
                {
                    new SdkUtilities.ConfigurableMockSdkResolver(
                        new SdkResultImpl(
                            sdk,
                            "path",
                            "2.0.0",
                            Enumerable.Empty<string>()))
                });
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Path.ShouldBe("path");
 
            _logger.WarningCount.ShouldBe(1);
            _logger.Warnings.First().Code.ShouldStartWith("MSB4241");
        }
 
        [Fact]
        public void AssertResolverThrows()
        {
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(includeErrorResolver: true));
 
            SdkReference sdk = new SdkReference("1sdkName", "version1", "minimumVersion");
 
            // When an SDK resolver throws, the expander will catch it and stop the build.
            SdkResolverException e = Should.Throw<SdkResolverException>(() => SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true));
            e.Resolver.Name.ShouldBe("MockSdkResolverThrows");
            e.Sdk.Name.ShouldBe("1sdkName");
        }
 
        [Fact]
        // Scenario: MockSdkResolverWithResolvableSdkPattern2 is a specific resolver (i.e. resolver with pattern)
        // and it successfully resolves sdk.
        public void AssertSecondResolverWithPatternCanResolve()
        {
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(includeResolversWithPatterns: true));
 
            SdkReference sdk = new SdkReference("2sdkName", "referencedVersion", "minimumVersion");
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Path.ShouldBe("resolverpathwithresolvablesdkpattern2");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolverWithResolvableSdkPattern2 running");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldNotContain("MockSdkResolver1 running");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldNotContain("MockSdkResolver2 running");
        }
 
        [Fact]
        // Scenario: MockSdkResolverWithResolvableSdkPattern1 is a specific resolver, it is loaded but did not resolve sdk.
        // MockSdkResolver1 is a general resolver (i.e. resolver without pattern), it resolves sdk on a fallback.
        public void AssertFirstResolverCanResolve()
        {
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy());
 
            SdkReference sdk = new SdkReference("1sdkName", "referencedVersion", "minimumVersion");
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Path.ShouldBe("resolverpath1");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolver1 running");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldNotContain("MockSdkResolverWithResolvableSdkPattern1 running");
        }
 
        [Fact]
        // Scenario: MockSdkResolver1 has higher priority than MockSdkResolverWithResolvableSdkPattern1 and resolves sdk.
        public void AssertFirstResolverWithPatternCantResolveChangeWave17_4()
        {
            using (TestEnvironment env = TestEnvironment.Create())
            {
                ChangeWaves.ResetStateForTests();
                env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave17_4.ToString());
                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
 
                SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(includeResolversWithPatterns: true));
 
                SdkReference sdk = new SdkReference("1sdkName", "referencedVersion", "minimumVersion");
 
                var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
                result.Path.ShouldBe("resolverpath1");
                _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolver1 running");
                _logger.BuildMessageEvents.Select(i => i.Message).ShouldNotContain("MockSdkResolverWithResolvableSdkPattern1 running");
                ChangeWaves.ResetStateForTests();
            }
        }
 
        [Fact]
        // Scenario: MockSdkResolver1 has higher priority than MockSdkResolverWithResolvableSdkPattern1 but MockSdkResolverWithResolvableSdkPattern1 resolves sdk,
        // becuase MockSdkResolver1 is general and MockSdkResolverWithResolvableSdkPattern1 is specific.
        public void AssertFirstResolverWithPatternCanResolve()
        {
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(includeResolversWithPatterns: true));
 
            SdkReference sdk = new SdkReference("11sdkName", "referencedVersion", "minimumVersion");
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Path.ShouldBe("resolverpathwithresolvablesdkpattern1");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolverWithResolvableSdkPattern1 running");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldNotContain("MockSdkResolver1 running");
        }
 
        [Fact]
        public void AssertSdkResolutionMessagesAreLogged()
        {
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy());
            SdkReference sdk = new SdkReference("1sdkName", "referencedVersion", "minimumVersion");
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            // First resolver attempted to resolve, but failed.
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("SDKResolverAttempt", nameof(MockResolverReturnsNull), sdk.ToString(), "null",
                ResourceUtilities.FormatResourceStringStripCodeAndKeyword("SDKResolverReturnedNull", nameof(MockResolverReturnsNull))));
            // Second resolver succeeded.
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("SucceededToResolveSDK", sdk.ToString(), nameof(MockSdkResolver1), result.Path, result.Version));
        }
 
        [Fact]
        public void AssertSdkResolutionMessagesAreLoggedInEventSource()
        {
            using var eventSourceTestListener = new EventSourceTestHelper();
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(false, false, true));
            var sdkName = Guid.NewGuid().ToString();
            SdkReference sdk = new SdkReference(sdkName, "referencedVersion", "minimumVersion");
 
            SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
            var eventsLogged = eventSourceTestListener.GetEvents();
            eventsLogged.ShouldContain(x => x.EventId == 64); // Start of the sdk resolve
            eventsLogged.ShouldContain(x => x.EventId == 65 && x.Payload[1].ToString() == sdkName);
        }
 
        [Fact]
        public void AssertFirstResolverErrorsSupressedWhenResolved()
        {
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy());
 
            // 2sdkName will cause MockSdkResolver1 to fail with an error reason. The error will not
            // be logged because MockSdkResolver2 will succeed.
            SdkReference sdk = new SdkReference("2sdkName", "version2", "minimumVersion");
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Path.ShouldBe("resolverpath2");
 
            // Both resolvers should run, and no ERROR string.
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolver1 running");
            _logger.BuildMessageEvents.Select(i => i.Message).ShouldContain("MockSdkResolver2 running");
 
            // Resolver2 gives a warning on success or failure.
            _logger.Warnings.Select(i => i.Message).ShouldBe(new[] { "WARNING2" });
            _logger.ErrorCount.ShouldBe(0);
        }
 
        [Fact]
        public void AssertResolverHasStatePreserved()
        {
            const int submissionId = 5;
 
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy());
 
            SdkReference sdk = new SdkReference("othersdk", "1.0", "minimumVersion");
 
            // First call should not know state
            SdkResolverService.Instance.ResolveSdk(submissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true).Path.ShouldBe("resolverpath");
 
            // Second call should have received state
            SdkResolverService.Instance.ResolveSdk(submissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true).Path.ShouldBe(MockSdkResolverWithState.Expected);
        }
 
        [Fact]
        public void AssertResolverStateNotPreserved()
        {
            const int submissionId = BuildEventContext.InvalidSubmissionId;
 
            SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy());
 
            SdkReference sdk = new SdkReference("othersdk", "1.0", "minimumVersion");
 
            // First call should not know state
            SdkResolverService.Instance.ResolveSdk(submissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true).Path.ShouldBe("resolverpath");
 
            // Second call should have received state
            SdkResolverService.Instance.ResolveSdk(submissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true).Path.ShouldBe("resolverpath");
        }
 
        [Fact]
        public void AssertResolversLoadedIfDefaultResolverSucceeds()
        {
            const int submissionId = BuildEventContext.InvalidSubmissionId;
 
            MockLoaderStrategy mockLoaderStrategy = new MockLoaderStrategy(includeDefaultResolver: true);
            SdkResolverService.Instance.InitializeForTests(mockLoaderStrategy);
 
            SdkReference sdk = new SdkReference("notfound", "1.0", "minimumVersion");
 
            SdkResolverService.Instance.ResolveSdk(submissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true).Path.ShouldBe("defaultpath");
 
#if NETCOREAPP
            // On Core, we check the default resolver *first*, so regular resolvers are not loaded.
            mockLoaderStrategy.ResolversHaveBeenLoaded.ShouldBeFalse();
            mockLoaderStrategy.ManifestsHaveBeenLoaded.ShouldBeFalse();
#else
            // On Framework, the default resolver is a fallback, so regular resolvers will have been loaded.
            mockLoaderStrategy.ResolversHaveBeenLoaded.ShouldBeTrue();
            mockLoaderStrategy.ManifestsHaveBeenLoaded.ShouldBeTrue();
#endif
        }
 
        [Theory]
        [InlineData(null, "1.0", true)]
        [InlineData("1.0", "1.0", true)]
        [InlineData("1.0-preview", "1.0-PrEvIeW", true)]
        [InlineData("1.0", "1.0.0", false)]
        [InlineData("1.0", "1.0.0.0", false)]
        [InlineData("1.0.0", "1.0.0.0", false)]
        [InlineData("1.2.0.0", "1.0.0.0", false)]
        [InlineData("1.2.3.0", "1.2.0.0", false)]
        [InlineData("1.2.3.4", "1.2.3.0", false)]
        public void IsReferenceSameVersionTests(string version1, string version2, bool expected)
        {
            SdkReference sdk = new SdkReference("Microsoft.NET.Sdk", version1, null);
 
            SdkResolverService.IsReferenceSameVersion(sdk, version2).ShouldBe(expected);
        }
 
        [Fact]
        public void CachingWrapperShouldWarnWhenMultipleVersionsAreReferenced()
        {
            var sdk = new SdkReference("foo", "1.0.0", null);
 
            var resolver = new SdkUtilities.ConfigurableMockSdkResolver(
                new SdkResultImpl(
                    sdk,
                    "path",
                    "1.0.0",
                    Enumerable.Empty<string>()));
 
            var service = new CachingSdkResolverService();
            service.InitializeForTests(
                null,
                new List<SdkResolver>
                {
                    resolver
                });
 
            var result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
            resolver.ResolvedCalls.Count.ShouldBe(1);
            result.Path.ShouldBe("path");
            result.Version.ShouldBe("1.0.0");
            _logger.WarningCount.ShouldBe(0);
 
            result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, new SdkReference("foo", "2.0.0", null), _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
            resolver.ResolvedCalls.Count.ShouldBe(1);
            result.Path.ShouldBe("path");
            result.Version.ShouldBe("1.0.0");
            _logger.WarningCount.ShouldBe(1);
            _logger.Warnings.First().Code.ShouldBe("MSB4240");
 
            resolver.ResolvedCalls.First().Key.ShouldBe("foo");
            resolver.ResolvedCalls.Count.ShouldBe(1);
        }
 
        private void CreateMockSdkResultPropertiesAndItems(out Dictionary<string, string> propertiesToAdd, out Dictionary<string, SdkResultItem> itemsToAdd)
        {
            propertiesToAdd = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                {
                    {"PropertyFromSdkResolver", "ValueFromSdkResolver" }
                };
 
            itemsToAdd = new Dictionary<string, SdkResultItem>(StringComparer.OrdinalIgnoreCase)
                {
                    { "ItemNameFromSdkResolver", new SdkResultItem( "ItemValueFromSdkResolver",
                        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                        {
                            { "MetadataName", "MetadataValue" }
                        })
                    }
                };
        }
 
        private void ValidateExpectedPropertiesAndItems(bool includePropertiesAndItems, SdkResultBase result)
        {
            if (includePropertiesAndItems)
            {
                result.PropertiesToAdd.Count.ShouldBe(1);
                result.PropertiesToAdd["PropertyFromSdkResolver"].ShouldBe("ValueFromSdkResolver");
 
                result.ItemsToAdd.Count.ShouldBe(1);
                result.ItemsToAdd.Keys.Single().ShouldBe("ItemNameFromSdkResolver");
                result.ItemsToAdd.Values.Single().ItemSpec.ShouldBe("ItemValueFromSdkResolver");
                var metadata = result.ItemsToAdd.Values.Single().Metadata;
                metadata.ShouldBeSameIgnoringOrder(new[] { new KeyValuePair<string, string>("MetadataName", "MetadataValue") });
            }
            else
            {
                result.PropertiesToAdd.ShouldBeNull();
                result.ItemsToAdd.ShouldBeNull();
            }
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void SdkResolverCanReturnNoPaths(bool includePropertiesAndItems)
        {
            var sdk = new SdkReference("foo", null, null);
 
            Dictionary<string, string> propertiesToAdd = null;
            Dictionary<string, SdkResultItem> itemsToAdd = null;
 
            if (includePropertiesAndItems)
            {
                CreateMockSdkResultPropertiesAndItems(out propertiesToAdd, out itemsToAdd);
            }
 
            var resolver = new SdkUtilities.ConfigurableMockSdkResolver(
                new SdkResultImpl(
                    sdk,
                    Enumerable.Empty<string>(),
                    version: null,
                    propertiesToAdd,
                    itemsToAdd,
                    warnings: null));
 
            SdkResolverService.Instance.InitializeForTests(null, new List<SdkResolver>() { resolver });
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Success.ShouldBeTrue();
            result.Path.ShouldBeNull();
            result.Version.ShouldBeNull();
 
            result.AdditionalPaths.ShouldBeNull();
 
            ValidateExpectedPropertiesAndItems(includePropertiesAndItems, result);
 
            _logger.WarningCount.ShouldBe(0);
        }
 
        [Fact]
        public void SdkResultCanReturnPropertiesAndItems()
        {
            string expectedPath = "Path/To/Return/From/Resolver";
 
            var sdk = new SdkReference("foo", null, null);
 
            Dictionary<string, string> propertiesToAdd;
            Dictionary<string, SdkResultItem> itemsToAdd;
 
            CreateMockSdkResultPropertiesAndItems(out propertiesToAdd, out itemsToAdd);
 
            var resolver = new SdkUtilities.ConfigurableMockSdkResolver(
                new SdkResultImpl(
                    sdk,
                    new[] { expectedPath },
                    version: "1.0",
                    propertiesToAdd,
                    itemsToAdd,
                    warnings: null));
 
            SdkResolverService.Instance.InitializeForTests(null, new List<SdkResolver>() { resolver });
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Success.ShouldBeTrue();
            result.Path.ShouldBe(expectedPath);
            result.Version.ShouldBe("1.0");
 
            result.AdditionalPaths.ShouldBeNull();
 
            ValidateExpectedPropertiesAndItems(true, result);
 
            _logger.WarningCount.ShouldBe(0);
        }
 
        [Theory]
        [InlineData(true)]
        [InlineData(false)]
        public void SdkResultCanReturnMultiplePaths(bool includePropertiesAndItems)
        {
            string expectedPath1 = "First/Path/To/Return/From/Resolver";
            string expectedPath2 = "Second/Path/To/Return/From/Resolver";
 
            var sdk = new SdkReference("foo", "1.0", null);
 
            Dictionary<string, string> propertiesToAdd = null;
            Dictionary<string, SdkResultItem> itemsToAdd = null;
 
            if (includePropertiesAndItems)
            {
                CreateMockSdkResultPropertiesAndItems(out propertiesToAdd, out itemsToAdd);
            }
 
            var resolver = new SdkUtilities.ConfigurableMockSdkResolver(
                new SdkResultImpl(
                    sdk,
                    new[]
                    {
                        expectedPath1,
                        expectedPath2
                    },
                    version: "1.0",
                    propertiesToAdd,
                    itemsToAdd,
                    warnings: null));
 
            SdkResolverService.Instance.InitializeForTests(null, new List<SdkResolver>() { resolver });
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Success.ShouldBeTrue();
 
            var resultPaths = new List<string>();
            resultPaths.Add(result.Path);
            resultPaths.AddRange(result.AdditionalPaths);
 
            resultPaths.ShouldBeSameIgnoringOrder(new[]
            {
                expectedPath1,
                expectedPath2
            });
 
            ValidateExpectedPropertiesAndItems(includePropertiesAndItems, result);
 
            _logger.WarningCount.ShouldBe(0);
        }
 
        [Fact]
        public void AssertResolutionWarnsIfResolvedVersionIsDifferentFromReferencedVersionWithMultipleReturnPaths()
        {
            var expectedPath1 = "First/Path/To/Return/From/Resolver";
            var expectedPath2 = "Second/Path/To/Return/From/Resolver";
 
            var sdk = new SdkReference("foo", "1.0", null);
            Dictionary<string, string> propertiesToAdd;
            Dictionary<string, SdkResultItem> itemsToAdd;
            CreateMockSdkResultPropertiesAndItems(out propertiesToAdd, out itemsToAdd);
 
            var resolver = new SdkUtilities.ConfigurableMockSdkResolver(
                new SdkResultImpl(
                    sdk,
                    new[]
                    {
                        expectedPath1,
                        expectedPath2
                    },
                    version: "1.1",
                    propertiesToAdd,
                    itemsToAdd,
                    warnings: null));
 
            SdkResolverService.Instance.InitializeForTests(null, new List<SdkResolver>() { resolver });
 
            var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true);
 
            result.Success.ShouldBeTrue();
 
            var resultPaths = new List<string>();
            resultPaths.Add(result.Path);
            resultPaths.AddRange(result.AdditionalPaths);
 
            resultPaths.ShouldBeSameIgnoringOrder(new[]
            {
                expectedPath1,
                expectedPath2
            });
 
            ValidateExpectedPropertiesAndItems(true, result);
 
            _logger.WarningCount.ShouldBe(1);
            _logger.Warnings.First().Code.ShouldStartWith("MSB4241");
        }
 
        /// <summary>
        /// Verifies that an SDK resolver is only called once per build per SDK.
        /// </summary>
        [Fact]
        public void CachingWrapperShouldOnlyResolveOnce()
        {
            var sdk = new SdkReference("foo", "1.0.0", null);
 
            var resolver = new SdkUtilities.ConfigurableMockSdkResolver(
                new SdkResultImpl(
                    sdk,
                    "path",
                    "1.0.0",
                    Enumerable.Empty<string>()));
 
            var service = new CachingSdkResolverService();
            service.InitializeForTests(
                null,
                new List<SdkResolver>
                {
                    resolver
                });
 
            // Resolve the SDK 10 times in parallel
            Parallel.For(
                0,
                10,
                _ => service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true));
 
            var result = resolver.ResolvedCalls.ShouldHaveSingleItem();
 
            result.Key.ShouldBe(sdk.Name);
 
            // The SDK resolver keeps track of the number of times it was called which should be 1
            result.Value.ShouldBe(1, $"The SDK resolver should have only been called once but was called {result.Value} times");
        }
 
        [Fact]
        public void InteractiveIsSetForResolverContext()
        {
            // Start with interactive false
            bool interactive = false;
 
            var service = new CachingSdkResolverService();
 
            service.InitializeForTests(
                resolvers: new List<SdkResolver>
                {
                    new SdkUtilities.ConfigurableMockSdkResolver((sdkRference, resolverContext, factory) =>
                    {
                        interactive = resolverContext.Interactive;
 
                        return null;
                    })
                });
 
            service.ResolveSdk(
                BuildEventContext.InvalidSubmissionId,
                new SdkReference("foo", "1.0.0", null),
                _loggingContext,
                new MockElementLocation("file"),
                "sln",
                "projectPath",
                // Pass along interactive and expect it to be received in the SdkResolverContext
                interactive: true,
                isRunningInVisualStudio: false,
                failOnUnresolvedSdk: true);
 
            interactive.ShouldBeTrue();
        }
 
        [Fact]
        public void IsRunningInVisualStudioIsSetForResolverContext()
        {
            bool isRunningInVisualStudio = false;
 
            var service = new CachingSdkResolverService();
            service.InitializeForTests(
                resolvers: new List<SdkResolver>
                {
                    new SdkUtilities.ConfigurableMockSdkResolver((sdkRference, resolverContext, factory) =>
                    {
                        isRunningInVisualStudio = resolverContext.IsRunningInVisualStudio;
                        return null;
                    })
                });
 
            var result = service.ResolveSdk(
                BuildEventContext.InvalidSubmissionId,
                new SdkReference("foo", "1.0.0", null),
                _loggingContext,
                new MockElementLocation("file"),
                "sln",
                "projectPath",
                false,
                // Pass along isRunningInVisualStudio and expect it to be received in the SdkResolverContext
                isRunningInVisualStudio: true,
                failOnUnresolvedSdk: true);
 
            isRunningInVisualStudio.ShouldBeTrue();
        }
 
        private sealed class MockLoaderStrategy : SdkResolverLoader
        {
            private List<SdkResolver> _resolvers;
            private List<SdkResolver> _defaultResolvers;
            private List<(string ResolvableSdkPattern, SdkResolver Resolver)> _resolversWithPatterns;
 
            public bool ResolversHaveBeenLoaded { get; private set; } = false;
            public bool ManifestsHaveBeenLoaded { get; private set; } = false;
 
            public MockLoaderStrategy(bool includeErrorResolver = false, bool includeResolversWithPatterns = false, bool includeDefaultResolver = false) : this()
            {
                if (includeErrorResolver)
                {
                    _resolvers.Add(new MockSdkResolverThrows());
                }
 
                if (includeResolversWithPatterns)
                {
                    _resolversWithPatterns.Add(("1.*", new MockSdkResolverWithResolvableSdkPattern1()));
                    _resolversWithPatterns.Add((".*", new MockSdkResolverWithResolvableSdkPattern2()));
                }
 
                if (includeDefaultResolver)
                {
                    _defaultResolvers.Add(new MockSdkResolverDefault());
                }
            }
 
            private MockLoaderStrategy()
            {
                _resolvers = new List<SdkResolver>
                {
                    new MockSdkResolver1(),
                    new MockSdkResolver2(),
                    new MockResolverReturnsNull(),
                    new MockSdkResolverWithState()
                };
 
                _defaultResolvers = new List<SdkResolver>();
 
                _resolversWithPatterns = new List<(string ResolvableSdkPattern, SdkResolver Resolver)>();
            }
 
            internal override IReadOnlyList<SdkResolver> LoadAllResolvers(ElementLocation location)
            {
                ResolversHaveBeenLoaded = true;
 
                return _resolvers.OrderBy(i => i.Priority).ToList();
            }
 
            internal override IReadOnlyList<SdkResolverManifest> GetResolversManifests(ElementLocation location)
            {
                ManifestsHaveBeenLoaded = true;
 
                var manifests = new List<SdkResolverManifest>();
                foreach (SdkResolver resolver in _resolvers)
                {
                    SdkResolverManifest sdkResolverManifest = new SdkResolverManifest(resolver.Name, null, null);
                    manifests.Add(sdkResolverManifest);
                }
                foreach ((string ResolvableSdkPattern, SdkResolver Resolver) pair in _resolversWithPatterns)
                {
                    SdkResolverManifest sdkResolverManifest = new SdkResolverManifest(
                        pair.Resolver.Name,
                        null,
                        new Regex(pair.ResolvableSdkPattern, RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(500)));
                    manifests.Add(sdkResolverManifest);
                }
                return manifests;
            }
 
            protected internal override IReadOnlyList<SdkResolver> LoadResolversFromManifest(SdkResolverManifest manifest, ElementLocation location)
            {
                ResolversHaveBeenLoaded = true;
 
                var resolvers = new List<SdkResolver>();
                foreach (var resolver in _resolvers)
                {
                    if (resolver.Name == manifest.DisplayName)
                    {
                        resolvers.Add(resolver);
                    }
                }
                foreach (var pair in _resolversWithPatterns)
                {
                    if (pair.Resolver.Name == manifest.DisplayName)
                    {
                        resolvers.Add(pair.Resolver);
                    }
                }
                return resolvers.OrderBy(t => t.Priority).ToList();
            }
 
            internal override IReadOnlyList<SdkResolver> GetDefaultResolvers()
            {
                return _defaultResolvers;
            }
        }
 
        private sealed class MockResolverReturnsNull : SdkResolver
        {
            public override string Name => nameof(MockResolverReturnsNull);
 
            public override int Priority => -1;
 
            public override SdkResultBase Resolve(SdkReference sdkReference, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory) => null;
        }
 
        private sealed class MockSdkResolver1 : SdkResolver
        {
            public override string Name => nameof(MockSdkResolver1);
 
            public override int Priority => 1;
 
            public override SdkResultBase Resolve(SdkReference sdk, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                resolverContext.Logger.LogMessage("MockSdkResolver1 running", MessageImportance.Normal);
 
                if (sdk.Name.StartsWith("1"))
                {
                    return factory.IndicateSuccess("resolverpath1", "version1");
                }
 
                return factory.IndicateFailure(new[] { "ERROR1" });
            }
        }
 
        private sealed class MockSdkResolver2 : SdkResolver
        {
            public override string Name => nameof(MockSdkResolver2);
 
            public override int Priority => 2;
 
            public override SdkResultBase Resolve(SdkReference sdk, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                resolverContext.Logger.LogMessage("MockSdkResolver2 running", MessageImportance.Normal);
 
                if (sdk.Name.StartsWith("2"))
                {
                    return factory.IndicateSuccess("resolverpath2", "version2", new[] { "WARNING2" });
                }
 
                return factory.IndicateFailure(new[] { "ERROR2" }, new[] { "WARNING2" });
            }
        }
 
        private sealed class MockSdkResolverWithResolvableSdkPattern1 : SdkResolver
        {
            public override string Name => nameof(MockSdkResolverWithResolvableSdkPattern1);
 
            public override int Priority => 2;
 
            public override SdkResultBase Resolve(SdkReference sdk, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                resolverContext.Logger.LogMessage("MockSdkResolverWithResolvableSdkPattern1 running", MessageImportance.Normal);
 
                if (sdk.Name.StartsWith("11"))
                {
                    return factory.IndicateSuccess("resolverpathwithresolvablesdkpattern1", "version3");
                }
 
                return factory.IndicateFailure(new[] { "ERROR3" });
            }
        }
 
        private sealed class MockSdkResolverWithResolvableSdkPattern2 : SdkResolver
        {
            public override string Name => nameof(MockSdkResolverWithResolvableSdkPattern2);
 
            public override int Priority => 0;
 
            public override SdkResultBase Resolve(SdkReference sdk, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                resolverContext.Logger.LogMessage("MockSdkResolverWithResolvableSdkPattern2 running", MessageImportance.Normal);
 
                if (sdk.Name.StartsWith("2"))
                {
                    return factory.IndicateSuccess("resolverpathwithresolvablesdkpattern2", "version4", new[] { "WARNING4" });
                }
 
                return factory.IndicateFailure(new[] { "ERROR4" }, new[] { "WARNING4" });
            }
        }
 
        private sealed class MockSdkResolverWithState : SdkResolver
        {
            public const string Expected = "01713226A202458F97D9074168DF2618";
 
            public override string Name => nameof(MockSdkResolverWithState);
 
            public override int Priority => 3;
 
            public override SdkResultBase Resolve(SdkReference sdkReference, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                if (sdkReference.Name.Equals("notfound"))
                {
                    return factory.IndicateFailure(new string[] { "notfound" });
                }
                if (resolverContext.State != null)
                {
                    return factory.IndicateSuccess((string)resolverContext.State, "1.0");
                }
 
                resolverContext.State = Expected;
 
                return factory.IndicateSuccess("resolverpath", "1.0");
            }
        }
 
        private sealed class MockSdkResolverThrows : SdkResolver
        {
            public override string Name => nameof(MockSdkResolverThrows);
            public override int Priority => 0;
 
            public override SdkResultBase Resolve(SdkReference sdk, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                resolverContext.Logger.LogMessage("MockSdkResolverThrows running", MessageImportance.Normal);
 
                throw new ArithmeticException("EXMESSAGE");
            }
        }
 
        private sealed class MockSdkResolverDefault : SdkResolver
        {
            public override string Name => nameof(MockSdkResolverDefault);
            public override int Priority => 9999;
 
            public override SdkResultBase Resolve(SdkReference sdk, SdkResolverContextBase resolverContext, SdkResultFactoryBase factory)
            {
                resolverContext.Logger.LogMessage("MockSdkResolverDefault running", MessageImportance.Normal);
 
                return factory.IndicateSuccess("defaultpath", string.Empty);
            }
        }
    }
}