diff --git a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs index 298987ef008..37ed394b4cd 100644 --- a/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs +++ b/src/Build.UnitTests/BackEnd/SdkResolverLoader_Tests.cs @@ -396,6 +396,114 @@ public void SdkResolverLoaderHonorsAdditionalResolversFolder() } } + /// + /// Test that LoadResolverAssembly handles fallback behavior correctly based on BuildEnvironment flags. + /// This test calls the actual LoadResolverAssembly method to ensure it fails when the fix is reverted. + /// + [Theory] + [InlineData(false, false)] // needsFallback = false (VS/MSBuild.exe), no fallback, should fail when Assembly.Load fails + [InlineData(true, true)] // needsFallback = true (API/dotnet CLI), has fallback, should succeed with LoadFrom + public void LoadResolverAssembly_MSBuildSdkResolver_WithAndWithoutFallback(bool needsFallback, bool shouldSucceed) + { + using (var env = TestEnvironment.Create(_output)) + { + // Save current BuildEnvironment to restore later + var currentBuildEnvironment = BuildEnvironmentHelper.Instance; + + try + { + // Setup BuildEnvironment based on test scenario + // needsFallback = true: Mode = Standalone && RunningInMSBuildExe = false (API/dotnet CLI) + // needsFallback = false: Mode = Standalone && RunningInMSBuildExe = true (MSBuild.exe direct usage) + // Note: We use Standalone mode for both cases to avoid VisualStudio mode requiring VisualStudioInstallRootDirectory + BuildEnvironmentMode mode = BuildEnvironmentMode.Standalone; + bool runningInMSBuildExe = !needsFallback; + + // Use current MSBuild path or fallback to a valid path if null + // This ensures MSBuildToolsDirectory32 and MSBuildToolsDirectoryRoot are set correctly + string msBuildExePath = currentBuildEnvironment.CurrentMSBuildExePath; + if (string.IsNullOrEmpty(msBuildExePath)) + { + // Use the executing assembly path as fallback + msBuildExePath = FileUtilities.ExecutingAssemblyPath; + // If that's also null/empty, use test assembly location + if (string.IsNullOrEmpty(msBuildExePath)) + { + msBuildExePath = typeof(BuildEnvironmentHelper).Assembly.Location; + } + } + + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly( + new BuildEnvironment( + mode, + msBuildExePath, + currentBuildEnvironment.RunningTests, + runningInMSBuildExe, + currentBuildEnvironment.RunningInVisualStudio, + currentBuildEnvironment.VisualStudioInstallRootDirectory)); + + // Create resolver folder structure with the specific name that triggers special logic + var testRoot = env.CreateFolder().Path; + var resolverFolder = Path.Combine(testRoot, "Microsoft.DotNet.MSBuildSdkResolver"); + Directory.CreateDirectory(resolverFolder); + + var assemblyFile = Path.Combine(resolverFolder, "Microsoft.DotNet.MSBuildSdkResolver.dll"); + + // Create file based on test scenario + if (shouldSucceed) + { + // For fallback test: create a valid assembly file using the test assembly + // This avoids side effects from loading Microsoft.Build.dll copy + var sourceAssembly = typeof(MockSdkResolverWithAssemblyPath).Assembly; + string sourceLocation = sourceAssembly.Location; + if (string.IsNullOrEmpty(sourceLocation)) + { + throw new InvalidOperationException("Source assembly location is null or empty"); + } + File.Copy(sourceLocation, assemblyFile, true); + } + else + { + // For no-fallback test: create invalid assembly content to force Assembly.Load to fail + File.WriteAllText(assemblyFile, "invalid assembly content"); + } + + // Use MockSdkResolverLoader but don't mock LoadResolverAssemblyFunc + // This ensures we test the actual logic in SdkResolverLoader.cs + var loader = new MockSdkResolverLoader + { + FindPotentialSdkResolversFunc = (_, __) => new List { assemblyFile }, + GetResolverTypesFunc = assembly => new[] { typeof(MockSdkResolverWithAssemblyPath) } + // LoadResolverAssemblyFunc is not set, so it will call the real method + }; + + if (shouldSucceed) + { + // Test that loading succeeds with fallback logic + var resolvers = loader.LoadAllResolvers(new MockElementLocation("file")); + resolvers.ShouldNotBeNull(); + resolvers.Count.ShouldBeGreaterThan(0); + } + else + { + // Should throw InvalidProjectFileException because: + // 1. needsFallback = false → no fallback, uses Assembly.Load directly + // 2. Assembly.Load fails on invalid assembly + // 3. No fallback → exception propagates + var exception = Should.Throw(() => + loader.LoadAllResolvers(new MockElementLocation("file"))); + + exception.Message.ShouldContain("could not be loaded"); + } + } + finally + { + // Restore original BuildEnvironment to avoid test pollution + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(currentBuildEnvironment); + } + } + } + private sealed class MockSdkResolverThatDoesNotLoad : SdkResolverBase { public const string ExpectedMessage = "A8BB8B3131D3475D881ACD3AF8D75BD6"; @@ -435,7 +543,13 @@ private sealed class MockSdkResolverWithAssemblyPath : SdkResolverBase { public string AssemblyPath; - public MockSdkResolverWithAssemblyPath(string assemblyPath = "") + // Parameterless constructor for reflection-based instantiation + public MockSdkResolverWithAssemblyPath() + : this("") + { + } + + public MockSdkResolverWithAssemblyPath(string assemblyPath) { AssemblyPath = assemblyPath; }