diff --git a/src/Build.UnitTests/BackEnd/SdkResolverService_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverService_Tests.cs index 62e842a2558..4a891408cc9 100644 --- a/src/Build.UnitTests/BackEnd/SdkResolverService_Tests.cs +++ b/src/Build.UnitTests/BackEnd/SdkResolverService_Tests.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Diagnostics.Tracing; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.BackEnd.SdkResolution; @@ -25,7 +27,7 @@ namespace Microsoft.Build.Engine.UnitTests.BackEnd { - public class SdkResolverService_Tests : IDisposable + public class SdkResolverService_Tests { private readonly MockLogger _logger; private readonly LoggingContext _loggingContext; @@ -41,20 +43,16 @@ public SdkResolverService_Tests() 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)); + var service = new SdkResolverService(); + service.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); + var result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); result.Success.ShouldBeFalse(); result.ShouldNotBeNull(); @@ -84,7 +82,8 @@ public void AssertResolutionWarnsIfResolvedVersionIsDifferentFromReferencedVersi { var sdk = new SdkReference("foo", "1.0.0", null); - SdkResolverService.Instance.InitializeForTests( + var service = new SdkResolverService(); + service.InitializeForTests( null, new List { @@ -96,7 +95,7 @@ public void AssertResolutionWarnsIfResolvedVersionIsDifferentFromReferencedVersi Enumerable.Empty())) }); - var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); + var result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); result.Path.ShouldBe("path"); @@ -107,12 +106,13 @@ public void AssertResolutionWarnsIfResolvedVersionIsDifferentFromReferencedVersi [Fact] public void AssertResolverThrows() { - SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(includeErrorResolver: true)); + var service = new SdkResolverService(); + service.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(() => SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true)); + SdkResolverException e = Should.Throw(() => service.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"); } @@ -122,11 +122,12 @@ public void AssertResolverThrows() // and it successfully resolves sdk. public void AssertSecondResolverWithPatternCanResolve() { - SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(includeResolversWithPatterns: true)); + var service = new SdkResolverService(); + service.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); + var result = service.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"); @@ -134,16 +135,63 @@ public void AssertSecondResolverWithPatternCanResolve() _logger.BuildMessageEvents.Select(i => i.Message).ShouldNotContain("MockSdkResolver2 running"); } +#if DEBUG + internal string TryResolveSdk(SdkResolverService service) + { + var message = ""; + SdkReference sdk = new SdkReference("2sdkName", "referencedVersion", "minimumVersion"); + try + { + service.ResolveSdk(BuildEventContext.InvalidSubmissionId, + sdk, + _loggingContext, + new MockElementLocation("file"), + "sln", + "projectPath", + interactive: false, + isRunningInVisualStudio: false, + failOnUnresolvedSdk: true); + } + catch (Exception e) + { + message = e.ToString(); + } + return message; + } + + + [Fact] + // Scenario: we want to test that we solved the contention described here: https://github.com/dotnet/msbuild/issues/7927#issuecomment-1232470838 + public async Task AssertResolverPopulationContentionNotPresent() + { + var service = new SdkResolverServiceTextExtension(); + service.InitializeForTests(new MockLoaderStrategy(includeResolversWithPatterns: true)); + + SdkReference sdk = new SdkReference("2sdkName", "referencedVersion", "minimumVersion"); + + var res1 = Task.Run(() => TryResolveSdk(service)); + + Thread.Sleep(200); + var res2 = Task.Run(() => TryResolveSdk(service)); + string message1 = await res1; + string message2 = await res2; + + Assert.Equal("", message1); + Assert.Equal("", message2); + } +#endif + [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()); + var service = new SdkResolverService(); + service.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); + var result = service.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"); @@ -155,11 +203,12 @@ public void AssertFirstResolverCanResolve() // becuase MockSdkResolver1 is general and MockSdkResolverWithResolvableSdkPattern1 is specific. public void AssertFirstResolverWithPatternCanResolve() { - SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(includeResolversWithPatterns: true)); + var service = new SdkResolverService(); + service.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); + var result = service.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"); @@ -169,10 +218,11 @@ public void AssertFirstResolverWithPatternCanResolve() [Fact] public void AssertSdkResolutionMessagesAreLogged() { - SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy()); + var service = new SdkResolverService(); + service.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); + var result = service.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", @@ -185,11 +235,12 @@ public void AssertSdkResolutionMessagesAreLogged() public void AssertSdkResolutionMessagesAreLoggedInEventSource() { using var eventSourceTestListener = new EventSourceTestHelper(); - SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy(false, false, true)); + var service = new SdkResolverService(); + service.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); + service.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); @@ -198,13 +249,14 @@ public void AssertSdkResolutionMessagesAreLoggedInEventSource() [Fact] public void AssertFirstResolverErrorsSupressedWhenResolved() { - SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy()); + var service = new SdkResolverService(); + service.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); + var result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); result.Path.ShouldBe("resolverpath2"); @@ -222,15 +274,16 @@ public void AssertResolverHasStatePreserved() { const int submissionId = 5; - SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy()); + var service = new SdkResolverService(); + service.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"); + service.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); + service.ResolveSdk(submissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true).Path.ShouldBe(MockSdkResolverWithState.Expected); } [Fact] @@ -238,15 +291,16 @@ public void AssertResolverStateNotPreserved() { const int submissionId = BuildEventContext.InvalidSubmissionId; - SdkResolverService.Instance.InitializeForTests(new MockLoaderStrategy()); + var service = new SdkResolverService(); + service.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"); + service.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"); + service.ResolveSdk(submissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true).Path.ShouldBe("resolverpath"); } [Fact] @@ -255,11 +309,12 @@ public void AssertResolversLoadedIfDefaultResolverSucceeds() const int submissionId = BuildEventContext.InvalidSubmissionId; MockLoaderStrategy mockLoaderStrategy = new MockLoaderStrategy(includeDefaultResolver: true); - SdkResolverService.Instance.InitializeForTests(mockLoaderStrategy); + var service = new SdkResolverService(); + service.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"); + service.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. @@ -388,9 +443,10 @@ public void SdkResolverCanReturnNoPaths(bool includePropertiesAndItems) itemsToAdd, warnings: null)); - SdkResolverService.Instance.InitializeForTests(null, new List() { resolver }); + var service = new SdkResolverService(); + service.InitializeForTests(null, new List() { resolver }); - var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); + var result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); result.Success.ShouldBeTrue(); result.Path.ShouldBeNull(); @@ -424,9 +480,10 @@ public void SdkResultCanReturnPropertiesAndItems() itemsToAdd, warnings: null)); - SdkResolverService.Instance.InitializeForTests(null, new List() { resolver }); + var service = new SdkResolverService(); + service.InitializeForTests(null, new List() { resolver }); - var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); + var result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); result.Success.ShouldBeTrue(); result.Path.ShouldBe(expectedPath); @@ -470,9 +527,10 @@ public void SdkResultCanReturnMultiplePaths(bool includePropertiesAndItems) itemsToAdd, warnings: null)); - SdkResolverService.Instance.InitializeForTests(null, new List() { resolver }); + var service = new SdkResolverService(); + service.InitializeForTests(null, new List() { resolver }); - var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); + var result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); result.Success.ShouldBeTrue(); @@ -515,9 +573,10 @@ public void AssertResolutionWarnsIfResolvedVersionIsDifferentFromReferencedVersi itemsToAdd, warnings: null)); - SdkResolverService.Instance.InitializeForTests(null, new List() { resolver }); + var service = new SdkResolverService(); + service.InitializeForTests(null, new List() { resolver }); - var result = SdkResolverService.Instance.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); + var result = service.ResolveSdk(BuildEventContext.InvalidSubmissionId, sdk, _loggingContext, new MockElementLocation("file"), "sln", "projectPath", interactive: false, isRunningInVisualStudio: false, failOnUnresolvedSdk: true); result.Success.ShouldBeTrue(); @@ -639,6 +698,44 @@ public void IsRunningInVisualStudioIsSetForResolverContext() isRunningInVisualStudio.ShouldBeTrue(); } + internal sealed class SdkResolverServiceTextExtension : SdkResolverService + { + + internal bool _fake_initialization = false; + internal IReadOnlyList _fakeManifestRegistry; + + internal override void WaitIfTestRequires() + { + if (_fake_initialization) + { + Thread.Sleep(10); + } + } + internal override IReadOnlyList GetResolverManifests(ElementLocation location) + { + return _fakeManifestRegistry; + } + + internal override void InitializeForTests(SdkResolverLoader resolverLoader = null, IReadOnlyList resolvers = null) + { + if (resolverLoader != null) + { + _sdkResolverLoader = resolverLoader; + _fake_initialization = true; + List manifests = new List(); + for (int i = 1; i != 20; i++) + { + var man = new SdkResolverManifest(DisplayName: "TestResolversManifest", Path: null, ResolvableSdkRegex: new Regex("abc")); + manifests.Add(man); + man = new SdkResolverManifest(DisplayName: "TestResolversManifest", Path: null, null); + manifests.Add(man); + } + _fakeManifestRegistry = manifests.AsReadOnly(); + return; + } + } + } + private sealed class MockLoaderStrategy : SdkResolverLoader { private List _resolvers; diff --git a/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs b/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs index 8eda48978c8..a9ea6b37548 100644 --- a/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs +++ b/src/Build/BackEnd/Components/SdkResolution/SdkResolverService.cs @@ -48,12 +48,12 @@ internal class SdkResolverService : ISdkResolverService /// /// Stores the list of manifests of specific SDK resolvers which could be loaded. /// - private IList _specificResolversManifestsRegistry; + protected IReadOnlyList _specificResolversManifestsRegistry; /// /// Stores the list of manifests of general SDK resolvers which could be loaded. /// - private IList _generalResolversManifestsRegistry; + protected IReadOnlyList _generalResolversManifestsRegistry; /// /// Stores an which can load registered SDK resolvers. @@ -62,7 +62,7 @@ internal class SdkResolverService : ISdkResolverService /// Unless the 17.10 changewave is disabled, we use a singleton instance because the set of SDK resolvers /// is not expected to change during the lifetime of the process. /// - private SdkResolverLoader _sdkResolverLoader = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10) + protected SdkResolverLoader _sdkResolverLoader = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_10) ? CachingSdkResolverLoader.Instance : new SdkResolverLoader(); @@ -178,6 +178,7 @@ private SdkResult ResolveSdkUsingResolversWithPatternsFirst(int submissionId, Sd List matchingResolversManifests = new(); foreach (SdkResolverManifest manifest in _specificResolversManifestsRegistry) { + WaitIfTestRequires(); try { if (manifest.ResolvableSdkRegex.IsMatch(sdk.Name)) @@ -258,7 +259,7 @@ private SdkResult ResolveSdkUsingResolversWithPatternsFirst(int submissionId, Sd return new SdkResult(sdk, null, null); } - private List GetResolvers(IList resolversManifests, LoggingContext loggingContext, ElementLocation sdkReferenceLocation) + private List GetResolvers(IReadOnlyList resolversManifests, LoggingContext loggingContext, ElementLocation sdkReferenceLocation) { // Create a sorted by priority list of resolvers. Load them if needed. List resolvers = new List(); @@ -387,12 +388,18 @@ private bool TryResolveSdkUsingSpecifiedResolvers( return false; } + internal virtual void WaitIfTestRequires() { } + + // This is a convenience wrapper that we override for one test so that we don't introduce unnecessary #if DEBUG + // segments into the production code. + internal virtual IReadOnlyList GetResolverManifests(ElementLocation location) => _sdkResolverLoader.GetResolversManifests(location); + /// /// Used for unit tests only. This is currently only called through reflection in Microsoft.Build.Engine.UnitTests.TransientSdkResolution.CallResetForTests /// /// An to use for loading SDK resolvers. /// Explicit set of SdkResolvers to use for all SDK resolution. - internal void InitializeForTests(SdkResolverLoader resolverLoader = null, IReadOnlyList resolvers = null) + internal virtual void InitializeForTests(SdkResolverLoader resolverLoader = null, IReadOnlyList resolvers = null) { if (resolverLoader != null) { @@ -403,19 +410,21 @@ internal void InitializeForTests(SdkResolverLoader resolverLoader = null, IReadO _sdkResolverLoader = CachingSdkResolverLoader.Instance; } - _specificResolversManifestsRegistry = null; - _generalResolversManifestsRegistry = null; + List specificResolversManifestsRegistry = null; + List generalResolversManifestsRegistry = null; _manifestToResolvers = null; if (resolvers != null) { - _specificResolversManifestsRegistry = new List(); - _generalResolversManifestsRegistry = new List(); + specificResolversManifestsRegistry = new List(); + generalResolversManifestsRegistry = new List(); _manifestToResolvers = new Dictionary>(); SdkResolverManifest sdkResolverManifest = new SdkResolverManifest(DisplayName: "TestResolversManifest", Path: null, ResolvableSdkRegex: null); - _generalResolversManifestsRegistry.Add(sdkResolverManifest); + generalResolversManifestsRegistry.Add(sdkResolverManifest); _manifestToResolvers[sdkResolverManifest] = resolvers; + _generalResolversManifestsRegistry = generalResolversManifestsRegistry.AsReadOnly(); + _specificResolversManifestsRegistry = specificResolversManifestsRegistry.AsReadOnly(); } } @@ -466,8 +475,7 @@ private void RegisterResolversManifests(ElementLocation location) return; } - var allResolversManifests = _sdkResolverLoader.GetResolversManifests(location); - + IReadOnlyList allResolversManifests = GetResolverManifests(location); _manifestToResolvers = new Dictionary>(); SdkResolverManifest sdkDefaultResolversManifest = null; @@ -484,24 +492,37 @@ private void RegisterResolversManifests(ElementLocation location) } } + var specificResolversManifestsRegistry = new List(); + var generalResolversManifestsRegistry = new List(); + // Break the list of all resolvers manifests into two parts: manifests with specific and general resolvers. - _specificResolversManifestsRegistry = new List(); - _generalResolversManifestsRegistry = new List(); + // Since the collections are meant to be immutable, we have to only ever assign them when they're complete. + // Otherwise race can happen, see https://github.com/dotnet/msbuild/issues/7927 foreach (SdkResolverManifest manifest in allResolversManifests) { + WaitIfTestRequires(); + if (manifest.ResolvableSdkRegex == null) { - _generalResolversManifestsRegistry.Add(manifest); + generalResolversManifestsRegistry.Add(manifest); } else { - _specificResolversManifestsRegistry.Add(manifest); + specificResolversManifestsRegistry.Add(manifest); } } if (sdkDefaultResolversManifest != null) { - _generalResolversManifestsRegistry.Add(sdkDefaultResolversManifest); + generalResolversManifestsRegistry.Add(sdkDefaultResolversManifest); } + + // Until this is set(and this is under lock), the ResolveSdkUsingResolversWithPatternsFirst will always + // enter if branch leaving to this section. + // Then it will wait at the lock and return after we release it since the collections we have filled them before releasing the lock. + // The collections are never modified after this point. + // So I've made them ReadOnly + _specificResolversManifestsRegistry = specificResolversManifestsRegistry.AsReadOnly(); + _generalResolversManifestsRegistry = generalResolversManifestsRegistry.AsReadOnly(); } }